Delay vs Millis
First, let's get one thing straight: the Arduino Uno cannot actually multitask. It has a single-core ATmega328P processor running at 16MHz, which means it can only execute one instruction at a time. No magic, no wizardry – just one thing after another.
But here's where it gets interesting. While we can't do true multitasking, we can create such a convincing illusion that your projects will feel like they're doing multiple things simultaneously. The secret? Speed and timing
The delay() Trap That Catches Everyone
When most people want to blink an LED every second, they reach for the delay()
function:
digitalWrite(ledPin, HIGH);
delay(1000);
digitalWrite(ledPin, LOW);
delay(1000);
Looks innocent enough, right? Dead wrong.
When delay(1000)
runs, your Arduino becomes a digital brick for exactly 1000 milliseconds. It can't read buttons, can't check sensors, can't respond to anything.
Example: The Button That Never Works
Let me show you exactly what I mean. Here's a simple setup:
- LED on pin 5 (blinks every second)
- Button on pin 4
- Buzzer on pin 3 (should beep when button is pressed)
const int ledPin = 5;
const int buttonPin = 4;
const int buzzerPin = 3;
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
pinMode(buzzerPin, OUTPUT);
}
void loop() {
digitalWrite(ledPin, HIGH);
delay(500);
digitalWrite(ledPin, LOW);
delay(500);
if (digitalRead(buttonPin) == LOW) {
digitalWrite(buzzerPin, HIGH);
} else {
digitalWrite(buzzerPin, LOW);
}
}
Try running this code. Press the button. What happens? Most of the time... nothing. The buzzer might beep for a split second if you're lucky enough to press the button at the exact moment the Arduino checks it, but that's about 1 millisecond out of every 1000.
That's a 99.9% failure rate. Not exactly what we'd call reliable.
millis():
Instead of making your Arduino wait, it simply tells you how many milliseconds have passed since the program started.
Think of it this way: delay()
is like sitting in a waiting room staring at the wall for an hour. millis()
is like glancing at your watch while you continue doing other things.
How millis() Actually Works Under the Hood
Here's where it gets technical (but stick with me, it's worth it):
Your Arduino has a hardware timer called Timer0 that ticks away in the background, completely independent of your code. Every ~1.024 milliseconds, this timer overflows and triggers an interrupt. That interrupt increments a counter.
When you call millis()
, Arduino simply reads this counter, does some quick math, and returns the total milliseconds elapsed. The beauty? This entire process takes just a few microseconds.
While your code runs, sleeps, or even crashes, Timer0 keeps ticking. It's your Arduino's built-in timekeeper.
The millis() Solution
Now let's rewrite our LED and buzzer code using millis()
:
const int ledPin = 5;
const int buttonPin = 4;
const int buzzerPin = 3;
unsigned long previousMillis = 0;
const unsigned long interval = 1000;
bool ledState = LOW;
void setup() {
pinMode(ledPin, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
pinMode(buzzerPin, OUTPUT);
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
ledState = !ledState;
digitalWrite(ledPin, ledState);
}
if (digitalRead(buttonPin) == LOW) {
digitalWrite(buzzerPin, HIGH);
} else {
digitalWrite(buzzerPin, LOW);
}
}
Upload this code and test it. The difference is night and day. The LED blinks perfectly every second, but now the button responds instantly every time you press it.
Breaking Down the code
Let's dissect what's happening in that millis()
code:
The Timing Logic
if (currentMillis - previousMillis >= interval) {
previousMillis = currentMillis;
ledState = !ledState;
digitalWrite(ledPin, ledState);
}
This is the heart of non-blocking timing. We're asking: "Has enough time passed since I last blinked the LED?" If yes, we blink it and remember the current time. If no, we move on immediately.
The Speed Factor
Here's the kicker: this entire loop runs thousands of times per second. Between LED timing checks, your Arduino can:
- Read the button hundreds of times
- Check sensors
- Update displays
- Communicate with other devices
- Handle multiple timing events
When NOT to Use millis()
millis()
isn't always the answer. Don't use it when:
- You need microsecond precision (use
micros()
instead) - You're in an interrupt service routine (ISRs should be fast and simple)
- You actually want to pause everything (rare, but sometimes necessary)
Real-World Applications Where This Matters
This isn't just academic. Here are practical scenarios where millis()
saves the day:
- Home automation: Multiple sensors, timers, and outputs running independently
- Robot control: Motor timing while reading sensors and responding to commands
- Data logging: Regular sensor readings while handling user interface
- LED displays: Animation timing while processing input
- IoT projects: Network communication without blocking sensor readings