Forget Arduino delay() already!

The delay() function in Arduino is not just overused, but almost always used the wrong way. Let’s see an example and how to schedule simple tasks the correct way!

Update

A follow-up video is added with a ton of details, at the end of the post!

7 segment display clock

How delay() works?

The delay() waits for some given time. It means it does NOT let other things run. Let’s look at the Arduino Blink example:

void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // turn the LED on (HIGH is the voltage level)
  delay(1000);                      // wait for a second
  digitalWrite(LED_BUILTIN, LOW);   // turn the LED off by making the voltage LOW
  delay(1000);                      // wait for a second
}

As the example states, we WAIT for 1000 milliseconds. Turning a LED on or off takes almost no time to complete, but not zero! We will not blink at 2 Hz rate, but a slightly bit higher. This must be corrected!

How to replace delay()

The “obvious” solution would be just using an embedded operating system, for example FreeRTOS. An RTOS takes care of scheduling “tasks”, a.k.a. “things to do”. But 8-bit microcontrollers like the “old” Arduino Uno R3 or Nano can’t handle an RTOS, so we need something lightweight. Let’s check out the millis() function, and download the free Arduino cheat sheet for a quick reference!

The millis() function

We have the millis() function to get the “runtime”: the number of milliseconds elapsed since startup. The Arduino framework includes some extra code to handle serial communication and this timer in the background with the help of interrupts, so this timer is relative precise. We can measure time with the help of millis()!

Lightweight Arduino tasks

Now we have to check the time with millis(), instead of waiting with delay(). We could do this several ways, I prefer storing the “next run” value and check if the current time reached this. If it’s time to run, increase the task timestamp with the period to schedule its new run (and prevent running it again and again). Otherwise we can do other stuff! This short example shows the skeleton, and check out the video about the details!

uint32_t task;
uint32_t period;

void loop() {
  if(task <= millis())
  {
    task += period;
    // do stuff
  }
}

Of course we can extend this to multiple tasks with different periods! The following example has 2 tasks:

uint32_t task_a;
uint32_t period_a;
uint32_t task_b;
uint32_t period_b;

void loop() {
  if(task_a <= millis())
  {
    task_a += period_a;
    // do stuff
  }

  if(task_b <= millis())
  {
    task_b += period_b;
    // do some other stuff
  }
}

Want to learn more?

If you would like to learn more, check out my Embedded course, or the YouTube channel!

Also, the update video about the topic:

Known issues

Of course, the example can be replaced with a proper interrupt-driven implementation. It was chosen as an obvious, but very real example. It would have been lame to add some random delays and fake calculations.

Also, as the documentation mentions, the millis() counter overflows (resets to zero) in less, than 2 months. Most Arduino hobbyprojects won’t operate that long, but if your project would run longer, you can create your own uint64_t counter.

Last but not least: what happens when a task runs longer than it’s period or misses the deadline? This basic “scheduler” is not an RTOS, it can’t do advanced stuff! The best it can achieve is run the late task as many times it missed the deadline, but that’s it. You must manually check the runtime of each task.