Accuracy problem using millis() for repeating a 10ms loop

Arduino Uno Rev 3. IDE running on Windows 10 (Acer) laptop

My simple timed loop is:

unsigned long Oldmillis = 0;
const long DELAY = 10; // 10 ms

void setup() {
  
}

void loop() {
  
  unsigned long newmillis = millis();

  if (newmillis - Oldmillis >= DELAY) {
    Oldmillis = newmillis;
    
    // Tasks to be repeated are placed here.
  }
}

I noticed that the loop was being repeated usually every 10 ms but occasionally every 11ms. I tried different values of delay and found that where DELAY is a multiple of 4 the loop repeats every DELAY ms, but for other values it occasionally repeats after DELAY+1 ms.

I wrote code to test it, which you may find below with some results at the end. When the delay is a multiple of 4 there is no serial output.

Many thanks for reading so far. I would be most grateful if anyone can shed a light on this. I can get around the problem, but am unlikely to sleep well until I find out what I did wrong!

/*
 * The TIMED LOOP loop is intended to run once every Delay_ms ms, using millis() .
 * It appears that with some values of Delay_ms it runs 1 ms late
 * The code is intended to detect the late occurrences and count them. 
 * When there have been a sufficient number, LATE_COUNT,
 * of late occurrences, it prints out the results.
 * 
 */
unsigned long Oldtime = 0;
unsigned long Timestamp;
int Latecounter = 0;
int Changecounter = 0;
int Delay_ms;
const int START_DELAY_MS = 10;
const int LATE_COUNT = 10;
const int NUMSAMPLES = 4;

void setup(){
Delay_ms = START_DELAY_MS;
}

void loop() {
  unsigned long currenttime = millis();
  int pastime = currenttime - Oldtime;
  
  // TIMED LOOP
  if (pastime >= Delay_ms){ // should trigger when = Delay_ms, and usually does!
    if (pastime > Delay_ms) { // but sometimes it gets to more than Delay_ms
      ++Latecounter; // Up the counter when it got over Delay_ms
      if (Latecounter >= LATE_COUNT){ // when there are sufficient errors, print the results
      
      long elapsedtime = currenttime - Timestamp;
      Timestamp = currenttime;
      // PRINT RESULTS
        Serial.begin(250000); 
        Serial.print("With Delay_ms set to: ");
        Serial.print(Delay_ms);
        Serial.print(" we get ");
        Serial.print(Latecounter);
        Serial.print(" late occurences in ");
        Serial.print(elapsedtime);
        Serial.print(" ms. That's about 1 in ");
        Serial.print(elapsedtime/Latecounter);
        Serial.println(" ms.");
        Serial.print(" The most recent loop started at ");
        Serial.print(currenttime);
        Serial.println(" ms.");
        Serial.print(" The previous loop started at ");
        Serial.print(Oldtime);
        Serial.println(" ms.");
        Serial.print(" The most recent delay was ");
        Serial.print(pastime);
        Serial.println(" ms.");
        Serial.end();
        Latecounter = 0;
        ++Changecounter;
        if (Changecounter >= NUMSAMPLES){ // Is it time to change the delay?
          Changecounter = 0;
          ++Delay_ms;
        }
      }
    }
    Oldtime = currenttime;
  }
}

Here are some typical results from the serial output:

14:14:52.305 -> With Delay_ms set to: 10 we get 10 late occurences in 2390 ms. That's about 1 in 239 ms.
14:14:52.305 -> The most recent loop started at 2390 ms.
14:14:52.305 -> The previous loop started at 2379 ms.
14:14:52.305 -> The most recent delay was 11 ms.
14:14:54.875 -> With Delay_ms set to: 10 we get 10 late occurences in 2560 ms. That's about 1 in 256 ms.
14:14:54.875 -> The most recent loop started at 4950 ms.
14:14:54.875 -> The previous loop started at 4939 ms.
14:14:54.875 -> The most recent delay was 11 ms.
14:14:57.418 -> With Delay_ms set to: 10 we get 10 late occurences in 2560 ms. That's about 1 in 256 ms.
14:14:57.418 -> The most recent loop started at 7510 ms.
14:14:57.418 -> The previous loop started at 7499 ms.
14:14:57.418 -> The most recent delay was 11 ms.
14:14:59.976 -> With Delay_ms set to: 10 we get 10 late occurences in 2560 ms. That's about 1 in 256 ms.
14:14:59.976 -> The most recent loop started at 10070 ms.
14:14:59.976 -> The previous loop started at 10059 ms.
14:14:59.976 -> The most recent delay was 11 ms.
14:15:03.177 -> With Delay_ms set to: 11 we get 10 late occurences in 3200 ms. That's about 1 in 320 ms.
14:15:03.177 -> The most recent loop started at 13270 ms.
14:15:03.177 -> The previous loop started at 13258 ms.
14:15:03.177 -> The most recent delay was 12 ms.
14:15:06.381 -> With Delay_ms set to: 11 we get 10 late occurences in 3200 ms. That's about 1 in 320 ms.
14:15:06.381 -> The most recent loop started at 16470 ms.
14:15:06.381 -> The previous loop started at 16458 ms.
14:15:06.381 -> The most recent delay was 12 ms.
14:15:09.580 -> With Delay_ms set to: 11 we get 10 late occurences in 3200 ms. That's about 1 in 320 ms.
14:15:09.580 -> The most recent loop started at 19670 ms.
14:15:09.580 -> The previous loop started at 19658 ms.
14:15:09.580 -> The most recent delay was 12 ms.
14:15:12.800 -> With Delay_ms set to: 11 we get 10 late occurences in 3200 ms. That's about 1 in 320 ms.
14:15:12.800 -> The most recent loop started at 22870 ms.
14:15:12.800 -> The previous loop started at 22858 ms.
14:15:12.800 -> The most recent delay was 12 ms.

Why do you keep restarting the serial interface?

It is a known issue with millis() that can not really be fixed. Use micros() instead for more exact time if the time period is short enough (<70 minutes).

millis() is not perfect. It hits almost all integers but will occasionally skip one. That is why your "if" statement must have ">=" and not "==" in its condition. I have seen an explanation of this effect but forget where I saw it.

I don't know if micros() would be better. Try it and report back?

TheMemberFormerlyKnownAsAWOL:
Why do you keep restarting the serial interface?

Thanks for answering. I have tried it both ways(with Serial.begin just once in the setup, but I ended up like this because I was trying to ensure that nothing extra was going on in the background. (I am new to Arduino).

Many thanks aarg and vagg4088 - relieved that it is not just me!
I was interested by some of the results I got.

I think that your way of calculating the interval might lead to cumulative error.
Instead of

Oldmillis = newmillis;

you might try

Oldmillis += DELAY;

That way, even if the odd cycle turns out to be 11, you set the next cycle to begin 10 from when the last one began without the cumulative error.

vinceherman:
I think that your way of calculating the interval might lead to cumulative error.
Instead of

Oldmillis = newmillis;

you might try

Oldmillis += DELAY;

That way, even if the odd cycle turns out to be 11, you set the next cycle to begin 10 from when the last one began without the cumulative error.

Thank you!

Here is what I got using micros instead of millis. It is frequently off by 4 microseconds and that's not bad.

unsigned long newmicros, oldmicros;
const long DELAY = 10000;   // 10 ms;

void setup()
{
  Serial.begin(115200);
  oldmicros = micros();
}

void loop()
{
  newmicros = micros();

  if (newmicros - oldmicros >= DELAY)
  {
    if (newmicros - oldmicros != 10000)
      Serial.println(newmicros - oldmicros);
    oldmicros = newmicros;
  }
}