Limitations of delay() & How to Do Timers Correctly

Limitations of delay() & How to Do Timers Correctly © GPL3+

The Arduino delay() function has a (usually unintended) side effect, this lesson tells you what it is and how to avoid it if needed.

  • 4,467 views
  • 1 comment
  • 8 respects

Components and supplies

Apps and online services

About this project

Delay(): uses and limitations

First Example

Push one button and turn a LED ON for 15 secs (15000 milliseconds)

(Note: This is pseudo code, ignoring the syntax of "{" and so on,  concentrating on the flow of the code. Also we are assuming a perfect button without contact bounce. As it happens, most examples here will work with a "noisy" button, as the LED timing hides the bounce.)

if digitalread==Down then
    digitalWrite ON
    delay 15sec
    digitalWrite OFF

Results: It works, yes, but looking closely: The button can change up or down as much as you like in the 15 seconds - it simply is not looked at. The delay starts counting when button is pushed. If the button is still down when 15 seconds expire, the LED will blink Off for a very short time (the time it takes loop to start again). This may or may not be what you intended.

Second Example

The same with two buttons and two LEDs. The intention is each button controls one LED, each staying lit for 15 seconds.

if digitalreadA==Down then
    digitalWriteA ON
    delay 15 sec
    digitalWriteA OFF
if digitalreadB==Down then
    digitalWriteB ON
    delay 15 sec
    digitalWriteB OFF

Result: This does NOT WORK the way it usually is intended - whilst A or B is on, we are totally ignoring the other button. This code can not turn on both LEDs no matter how you push the buttons.

The code is not the problem either. As long as we are using delay() then no combination of if then and digitalRead will allow the choice of one or both LEDs by button pushes. This is why delay() is fatal and very wrong if you need to do more than one single thing at a time.

Using a millis() timer instead

How to get similar delay. Timer is not a feature or function, it is simply comparing millis() value regularly to determine when the desired time has elapsed.

Example 1: Implementing a simple timer

//Psudocode 
unsigned long Timer   
// ALWAYS use unsigned long for timers, not int
 
(variable declaration outside setup and loop, of course)
 
if digitalRead == Down  
    then "set timer"
    Timer = millis 
    digitalWrite ON
if "timer expired" 
    millis - Timer >= 15000UL  
    then digitalWrite Off
 
(the "UL" after the number is a syntax detail that is important when dealing with large numbers in millis and micros, therefore it is shown although this is pseudo code)

Result: It works just as well as the delay version, but slightly differently: The timer is constantly reset as long as the button is pushed. Thus only when releasing does it start to "count" the 15 seconds. If you push shortly inside the 15 seconds period, it starts counting from anew. Notice in particular that digitalRead is looking at the input as fast as loop() runs.

The code is doing a digitalWrite OFF, even when the LED is not lit for every loop() pass, and likewise digitalWrite ON as long as the button is pushed. Fortunately this has no ill effects (apart from taking a handful of microseconds time).

Example 2: Two buttons and LEDs

This is rather simple, we just "repeat" the above code.

//Psudocode 
unsigned long TimerA   
"ALWAYS use unsigned long for timers..."
unsigned long TimerB 
if digitalReadA==Down
  then  TimerA= millis() ,
  digitalWriteA ON
if millis()-TimerA >= 15000UL 
  then digitalWriteA Off
if digitalReadB==Down
  then  TimerB= millis() ,
  digitalWriteB ON
if millis()-TimerB >= 15000UL
  then digitalWriteB Off

Result: This works! Two separate timers start and stop independently.

As shown in the single button/LED case, we are done with handling the A case in just a few tens of microseconds, whether we are counting seconds or not, and thus free to handle the B case. We thus examine and react to either buttons hundreds of times each millisecond.

Interesting side issue: The 4 if statements can be freely rearranged - the end effect is the same. There are some subtle differences only active for the few microseconds until loop() goes round again.

More complex millis() timers

The effect that the timer is to start on button push or button release, or if a button should be ignored once the timer has started may be a requirement. Timer code can be made to handle all variations by storing the desired state and skipping digitalRead. This will work fine with two buttons, too.

Example 3: Timer with State change

//Psudocode 
boolean LEDon = false 
unsigned long Timer
"ALWAYS use unsigned long"
if not LEDon and digitalRead==Down
   then "set timer" Timer= millis 
   digitalWrite ON
   LEDon = true
if LEDon and millis-Timer >= 15000UL
   then digitalWrite Off
   LEDon = false 

Result: This will not do a digitalRead or reset the timer, once the LED has turned on, ie. we are starting the timer on button push (like attempt 1 earlier). Likewise it only tests the timer if the LED is on, and only turns it off once (which make very little difference, so I would normally omit the LED on test in the 2nd part).

And this will work fine with two or more buttons and LEDs, as the timers are independent and the code will not block.

Example 4: One button, one LED - each with their own timer.

A more useful example of doing two timing things "at once":

//Psudocode
boolean LEDon = false
boolean buttondown = false
unsigned long LEDtimer = 0
"ALWAYS use unsigned long..."
unsigned long buttontimer = 0  
if millis - buttontimer >= 5UL "debounce 5 millisec" 
   and not buttondown 
   and digitalReadButton==Down
         then buttondown = true 
         buttontimer = millis
if millis - buttontimer >= 5UL    
   and buttondown 
   and digitalReadButton==Up
         then buttondown = false
         buttontimer = millis
if not LEDon and buttondown   //can use "not buttondown"
         then LEDtimer = millis
         digitalWrite ON
         ledon = true
if millis - LEDtimer >= 15000UL  
         then digitalWrite Off
         LEDon = false 

Result: The button timer will purposefully skip digitalRead of the button for 5 milliseconds after it has changed. This filters away the "noise" in any mechanical button. The Led on/off code can be set to trigger on button down or up.

A Challenge

If you use a previousbuttonup indicator instead of LEDon, you can ensure that the button has to be released before a new timer cycle can start - but that is left as an exercise, and is more about state transition than about timers.

So, this was the last time you used delay(), right? There will be exceptions, of course.

Comments

Similar projects you might like

Internal Timers of Arduino

Project tutorial by Marcazzan_M

  • 12,149 views
  • 10 comments
  • 44 respects

Walk Away From Delay

Project tutorial by Tal O

  • 2,728 views
  • 6 comments
  • 11 respects

Arduino Kitchen Timer

Project tutorial by Team I and myself

  • 68,044 views
  • 43 comments
  • 90 respects

Master Slave I2C Connection

by PIYUSH_K_SINGH

  • 10,330 views
  • 0 comments
  • 11 respects
Add projectSign up / Login