Yet Another Tutorial On Timing With millis()/micros()

Since I’ve had success with developing an idea to preproduction prototype stage using Arduinos, I’m giving back to the community by spamming several subfora with things I’ve worked out through my dev process that I know people have asked about. (“I know people asked about” because I also asked, and found posts here where others have asked before me.) I’ll be posting various things in various places, so hopefully I’ll get the locations right, and if not, if a mod could make moves as needed that’d be lovely.

For this post, I’ll throw in yet another tutorial on using millis() and micros() for timing.

Let’s start with the raw definitions. These two functions return the number of milliseconds or microseconds that have elapsed since the program started running. Since they simply return a number, they don’t block code execution, and that means they’re wonderfully useful for situations where you have a loop running and need code inside that loop to fire off on a regular interval without hanging the loop in the process like a blocking function like delay() will.

The way we use these for timing is twofold: first, we take a snapshot of the current millis() or micros() value just before we enter a loop. Second, inside the loop we check to see what the difference is from the current millis()/micros value is versus the snapshot. By subtracting first value from current we get a value of how many milliseconds or microseconds have elapsed, and if enough time has passed, we replace the snapshot with a current value and wait for the difference to reach our desired interval value again. Rinse and repeat as needed.

Let’s look at a real-world example from running code. We start by declaring a variable for the snapshot - it must be an unsigned long, as that’s the return value for both millis() and micros().

  unsigned long snapshot_time = 0;

Just before the looping code within which time-delayed code will run, we’ll set the snapshot variable:

  snapshot_time = millis();

Inside the loop we’ll perform the following:

  if (millis() < snapshot_time)
    snapshot_time = 4294967295 - snapshot_time;

  if (millis() > snapshot_time + 150)
  {
    snapshot_time = millis();

    // Do work, son!
  }

The first two lines are there to deal with the fact that millis() and micros() will wrap around to zero after a while. Since they’re unsigned longs, the maximum value is 2^32 - 1, or 4294967295, so if millis()/micros() is less than the last snapshot taken, the millis()/micros() value has wrapped to zero and we have to subtract the last snapshot time from that limit to bring it back into range. If this isn’t done, the time-delayed code may never fire again once the millis() or micros() value wraps to zero!

Next, we have the comparison that determines how long to wait before executing the code inside the “if” condition. In the example we’re waiting for millis() to exceed the previous millis() reading plus 150 milliseconds, which means that the delay we’re getting as a result is at least 150 milliseconds. I say it’s “at least” because the timing granularity on slower Arduinos might not be adequate to be precise, and a long code loop takes longer to execute than a short one does, so never use an equals here, but instead always check for greater-than.

Once the current time is at least 150ms later than the previous time, we snapshot the millis() (or micros()) value again and run our delayed code. Once it finishes up, the loop continues and the code in the “if” is ignored until at least 150ms has elapsed since the previous, and so on and so forth.

Fun fact: An unsigned long containing milliseconds from boot will run to max and loop back to zero in about 49.7 days. Windows 95 had a bug in it that would hang the OS once its millisecond counter rolled over, because some of the OS components didn’t understand how to deal with the rollover!

Pretty straightforward and easy to use once you get the hang of it! Have more ideas on how to time events in a non-blocking manner? Post 'em below!

The first two lines are there to deal with the fact that millis() and micros() will wrap around to zero after a while. Since they're unsigned longs, the maximum value is 2^32 - 1, or 4294967295, so if millis()/micros() is less than the last snapshot taken, the millis()/micros() value has wrapped to zero and we have to subtract the last snapshot time from that limit to bring it back into range. If this isn't done, the time-delayed code may never fire again once the millis() or micros() value wraps to zero!

Come on - have you ever seen it done this way? have you looked at blink without delay? That’s a very poor recommendation.

Next, we have the comparison that determines how long to wait before executing the code inside the "if" condition. In the example we're waiting for millis() to exceed the previous millis() reading plus 150 milliseconds,

That’s a bad recommendation as well... what if adding 150 actually overflows and goes back to 0 (/a small number), then you trigger right away...

Just go do if (millis()-snapshot_time > 150) {... As long as you are using unsigned long for snapshot_time you will be fine even when millis() goes back to 0 because the subtraction is done with unsigned numbers. Don’t do addition, go for subtraction

Hopefully you are not dumping all over the forum lame recommendations and poor coding technics... looks you have not done even the basic research on that one.

No one wanting good code should follow this recommendation

Your code fires early as millis approaches the wrap.

Assume the current value of millis is 4294967195.

  snapshot_time = millis();
  snapshot_time = 4294967195;

The first if is false (millis and snapshot_time are equal).

The second if becomes this...

if (millis() > snapshot_time + 150)
if (4294967195 > 4294967195 + 150)
if (4294967195 > 49)

...which is true. Yet 150 milliseconds has not elapsed.

This problem has been analyzed to death. I suggest you stick with what has been shown to work reliably (subtraction to calculate elapsed time).

Then I shall stand corrected and adjust accordingly, as you both made excellent points that I'd apparently missed. Feel free to lock or remove the thread as needed.

You will find examples in Using millis() for timing. A beginners guide that do not have problems with rollover to zero