Why does this 'millis reset' fail?

For some time I've been using the 'reset' code recommended in Robin2's excellent tutorial here:
https://forum.arduino.cc/t/demonstration-code-for-several-things-at-the-same-time/217158
Instead of the familiar and widely used
previousMillis = currentMillis;
I use
previousMillis += interval;

I'd assumed that it was universal, but a current sketch I'm writing seems to prove otherwise. It works with the familiar method but causes triggering well before the set interval with the alternative.

The simple example below does the same. It arises when I set a non-zero value for previousMillis, (which I occasionally do when testing, to shorten an otherwise long delay before the first activation).

But I can't fathom the reason?


unsigned long currentMillis;
unsigned long previousMillis;
const unsigned long interval = 5000;

void setup()
{
  // Start serial communication for debugging purposes
  Serial.begin(115200);
  //  delay(200);
  Serial.println("Robin2Puzzle");
  Serial.println("========================================");
  Serial.println("Setup ended, millis = ");
  Serial.println(millis());
  previousMillis = 2; // Reset fails if this is 2 or more.
  // But the familiar method works regardless
}

void loop()
{
  currentMillis = millis();
  if (currentMillis - previousMillis >= interval)
  {
    Serial.println("Interval has ended");
    previousMillis += interval; // Reset; fails if line 20 set to
    // a value of 2 or more. Not clear why?
    //    previousMillis = currentMillis; // Reset; always works
  }
}

Code?

I imagine it's the same reason that the addition version of BWoD fails at rollover.

When you set millis to a non-zero number in the traditional BWoD code, it will trigger an event immediately because previousMillis is greater than the current time. At the beginning of the code if millis is giving 2 or 3 and previousMillis is set to 50 then it looks like the thing has been running for 49 days and it gets triggered and since previousMillis gets set to actual millis it will all get sorted once that happens.

If, on the other hand, you are adding to previousMillis as you describe. Then if you set previousMillis to 50 and millis is currently at 2 it looks like the thing has been on for 49 days and it triggers. Then you add say a minute to previousMillis. So now it's 60050. And millis has gone up to 4 or 5. So it still looks like it's been 49 days. Then again and again until the thing happens enough times to wrap previousMillis back around.

If you want to set previousMillis to a non-zero number with the addition style, then set it to something very large. Something just a little less than 0 - you timing period.

Which line is 20? I don't have line numbers. To me that's the if statement. An if statement can't be set to 20 so I don't quite follow.

Wasn't a good idea to use a line number, especially if I then edit it! I'll avoid it in future.
The line I meant is:
previousMillis = 2; // Reset fails if this is 2 or more.

Then yes, my answer in reply #1 is the issue. If you instead set previousMillis to:

previousMillis = 0 - (interval - 5);

Then you will get the behavior you expect I think. In this case it will trigger when millis is at 5. Then it adds 5000 and gets 5 for previousMillis. And that is right.

If previousMillis starts out at 2 and the first check is at 1, then the subtraction shows that the elapsed time is almost 49 days. That triggers and adds 5000 to previous millis. Now previousMillis is 5002 and millis is at 3 so it still looks like a long time and triggers again. Then previousMillis is at 10004 and millis is at 4. So it still looks like a long time. etc. etc.

2 Likes

Thanks, understood (just about!).

But I'll return to using the familiar and intuitive method.
:slightly_smiling_face:

these aren't the same thing if you do

will work for while if you do

 if (currentMillis >= previousMillis)

but when previousMillis wraps, currentMillis will be < until it also wraps

The idea is that you're playing a trick to pretend that something already happened in the past. If the board just started and millis is at 2, and you want to pretend that it's almost time to trigger your action, then what does previousMillis need to be? Pretend that instead of just starting up, it had just rolled over and think about what things would look like there. In that case previousMillis would be some very large number.

The whole idea of adding the interval to previousMillis instead of setting it equal to the current time is that if the timing code runs late and slightly misses an interval it will realign the next interval with where it should have been. Basically it catches up if it misses by a little. This is important if you are trying to keep two things in time that aren't being timed by the same chip. If you use the previousMillis = millis() style, then if the check happens a little bit late the whole sequence gets delayed by a little. It doesn't catch back up. And if it keeps happening then it will eventually seem to "run slow".

The issue with addition style is that if it completely misses some intervals then it will trigger as many times as it missed until previousMillis catches back up to millis. In the case where you set previousMillis to 2, you've tricked it into thinking it missed 49 days of intervals. So it's going to run a bunch of times trying to catch up.

If is because you are setting previousMillis into the future, so that the currentMillis - previousMillis term would be negative, and then rollover(rollunder?) into the near ULONG_MAX value, which will make the condition true, and then try to repeatedly increment previousMillis until the condition is false.

I'm partial to the previousMillis += interval form, since it absorbs rather than integrates any jitter caused by loop() delays. But if you try to use it with previousMillis set in the future, it will fail like this. If you do want to schedule future actions with a timestamp set in the future, you can cast the difference as signed, and use that:

unsigned long currentMillis;
unsigned long targetMillis;
const unsigned long interval = 5000;

void setup()
{
  // Start serial communication for debugging purposes
  Serial.begin(115200);
  //  delay(200);
  Serial.println("Robin2Puzzle");
  Serial.println("========================================");
  Serial.println("Setup ended, millis = ");
  Serial.println(millis());
  targetMillis = 2; // start regular intervals at ...}

void loop()
{
  currentMillis = millis();
  if ((signed long)(currentMillis - targetMillis) >=0)
  {
    Serial.println("Interval has ended at");
    Serial.println(currentMillis);
    targetMillis += interval; // advance target
  }
}

Here's a simulation using an array of 1000 individual future-based timestamps:

1 Like

Thanks. I think in practice I’ll use the familiar method while testing and then restore the superior method afterwards. Or I could simply use a much shorter interval during testing!

1 Like

You can test it just like you have it as long as you start with previousMillis at something that makes sense and not 2.

"Something that makes sense" just means not setting previousMillis to the recent past (like a few seconds ago) and not to the distant past (49 days ago). Try setting previousMillis to -5000 (or millis() - interval) if you want the timed action to trigger the very first time loop() is called, or to -4000 if you want it to trigger in one second, etc.

(-5000 will actually be converted to 4294962296 since previousMillis is unsigned long, but that's not something to care about at this point. All that matters is that modular arithmetic works.)

1 Like

Perhaps:

Yes, that's what I meant, thanks.

Thanks, got it!

Here's my revised sketch. Please let me know if there are any significant mistakes in my comments.

unsigned long currentMillis;
unsigned long previousMillis;
const unsigned long interval = 5000; // In the real program this
// will be an hour; 5000 chosen as the smallest that can be used
// during testing, without affecting other sections.
// To make the delay before the first execution even shorter,
// I set previousMillis to 4000 at end of setup(). As discussed,
// that fails if the 'better' reset method is used, for reasons
// now understood.

void setup()
{
  // Start serial communication for debugging purposes
  Serial.begin(115200);
  delay(200);
  Serial.println("Robin2PuzzleResolved");
  Serial.println("========================================");
  Serial.println("Setup almost ended, millis = ");
  Serial.println(millis());
  //  previousMillis = 202; // Reset will fail in this example
  // because that will already 'in the future'.
  previousMillis = -4000; // Resolves the problem, in this case
  // now waiting less than 1 second (5000 - 4200 ms).
}

void loop()
{
  currentMillis = millis();
  if (currentMillis - previousMillis >= interval)
  {
    Serial.println("Interval has ended");
    previousMillis += interval; // Reset; fails if
    // previousMillis was set to a positive value exceeding the
    // (unknown but small) period between end setup() and this
    // command's first execution; in this example 202 or more.
    // For reason I now understand!
    // But the familiar method below works regardless.
    // previousMillis = currentMillis; // Reset; always works,
    // However it could sometimes cause inaccurate synchronisation,
    // hence preference for the alternative.

    // Solution from @christop in post #12 of
    // https://forum.arduino.cc/t/why-does-this-millis-reset-fail/1147017/10
    // So in this case I have set previousMillis to -4000, resulting in
    // a mere 800 ms before the message appears after start-up or reset.
  }
}

Use a static previousMillis in the loop function, and initialise it to the value of millis.

void loop()
{
  static uint32_t previousMillis = millis ();  
  currentMillis = millis();
  if (currentMillis - previousMillis >= interval)   {

If you think about it for a bit, you might see that saying

    previousMillis = currentMillis;

is equivalent.

They are equivalent if… you don't take more than interval milliseconds getting through the code you are timing.

You should be getting through the controlled code in less time than you allow, by design. If you take more than interval milliseconds, you don't get regular execution of the code, you will skip beats so to speak.

Sometimes that matters, like if you were timing out a clock ticking sound.

If you do overrun the time slice, the behaviour of the two methods differs, it's a choice only the designer can make between

  • issuing the number of ticks that have been missed, but the time between them would be unregulated

and

  • doing one tick and just getting over having missed any

It would sound different. One would be a gap with missing ticks, then N ticks to catch up, at some uncontrolled pace, then back to ticking.

The latter would be the same silence, then back to ticking regularly.

Since you shouldn't be overrunning it shouldn't matter. But it's like a prescription medication. Do you take all the pills you forgot over the last day or two, or do you take you pills for the day and get over it.

Ask your doctor. But take your pills (design code that can run within the allotted time slice (interval)).

If the catch-up ticks were paying someone for every execution of the controlled code, she would want you to play catch up… and a clock might want you to goose it along N intervals so it regains accurate reading of time.

I always use the latter formulation and just forget about the missed ticks, but I plan to not miss any.

Where there is any risk of overrunning, I install a mechanism in the code so I can be made aware I'm not getting the work done in the time I thought it would take.

a7

1 Like

Historically, known as a 1202 :wink:

Well that's one I had to google.

Did you mean 12002?

a7

I don't think the display was wide enough for that!