Embedded Systems
How to do two things at once on an Arduino
An Arduino Uno has one processor, so it can't run two loops side by side. It doesn't need to. If nothing in loop() waits, loop() goes round thousands of times a second, and each pass gives every job a quick look: is it time to blink? Has the button changed? Each job keeps its own notes in its own variables, and none of them ever calls delay().
Why delay() breaks it
Blink with delay(500) twice and the board spends almost all its time waiting. It reads the button only between the waits, once a second. A press is answered up to a second late, and a quick 0.1 s tap is usually over before the read comes round.
Live experiment
The doorbell during the blink
Dot on pin 9 blinks once a second; Pip on pin 10 lights while the doorbell button is held. Written with delay(), a ring is answered late or missed. Written with millis(), loop() reads the button about every 46 µs and every ring is answered at once.
“Two jobs at once” is lesson 2 of 6 in Clockwork Cove, Embedded Systems World 2. Free with an account. Lessons unlock in order, so you’ll do the 8 before it first.
Two jobs, one loop
The blink glances at the clock; the button job reads the pin. Neither waits, so both run on every pass:
unsigned long lastBlink = 0;
unsigned long interval = 500;
bool ledOn = false;
bool last = HIGH;
unsigned long changedAt = 0;
void setup() {
pinMode(9, OUTPUT);
pinMode(2, INPUT_PULLUP);
}
void loop() {
// job 1: the blink
if (millis() - lastBlink >= interval) {
lastBlink = millis();
ledOn = !ledOn;
digitalWrite(9, ledOn);
}
// job 2: the button
bool now = digitalRead(2);
if (now != last && millis() - changedAt > 50) {
changedAt = millis();
last = now;
if (now == LOW) { // a new press
if (interval == 500) { interval = 125; }
else { interval = 500; }
}
}
}
Each press switches the blink between once a second and four times a second, and the LED never pauses.
Act on the change, not the level
The pin reads LOW on every pass while the button is held, thousands of times for one press. Toggle on every LOW and the setting flips thousands of times and lands at random. So the button job compares this pass's reading with last and acts only when it changes from HIGH to LOW.
Debounce without waiting
Real contacts bounce for a few milliseconds when they close and again when they open. Instead of delay(50), the sketch notes the time of each accepted change in changedAt and ignores the pin for 50 ms after it, on the press and on the release. Nothing waits, so the blink carries on through the bounce.
How quick is quick enough? If loop() runs 10,000 times a second, a 100 ms press is seen on about 1,000 passes. Nothing slips through.
Free lesson
Now build it yourself
Each lesson comes in three short parts: see the idea, try it on a live board, then build it yourself, with quick checks as you go.
“Two jobs at once” is lesson 2 of 6 in Clockwork Cove, Embedded Systems World 2. Free with an account. Lessons unlock in order, so you’ll do the 8 before it first.
Or start from the very first lesson, no account neededPractice questions
Three quick questions from the lessons. Pick an answer to check it.
Show the answer
Answer: On the very next pass of loop(). Nothing waits, so loop() goes round thousands of times a second and reads the button on every pass. A press is seen within microseconds.
- Why not “Once a second”? Once a second is what happens with delay(500) twice. Without delay(), loop() reads the button thousands of times a second.
- Why not “Never”? Nothing stops loop() reading the pin, so a press is always seen, within microseconds.
- Why not “After the blink finishes”? There's no waiting for the blink to finish. With millis() the blink is just a quick check, so the button is read on every pass.
Show the answer
Answer: Usually missed: loop() is still in a delay() when you let go. With delay(), loop() reads the button once a second, between the waits. A 0.1 s tap is usually over before it gets there. With millis(), the button is read every pass and nothing is missed.
- Why not “Answered twice”? A quick tap is read once at most. The trouble is that it's usually over before the read comes round.
- Why not “Answered after exactly 500 ms”? The read comes only when both waits are over, anywhere up to a second after the tap, and by then the tap is usually gone.
- Why not “Answered at once”? With delay(), loop() spends almost all its time waiting and reads the button only once a second, between the waits.
Show the answer
Answer: stop the blink for as long as the button is held. A while that waits is a delay in disguise. For as long as the button is held, loop() is stuck on that line and the blink's glance at the clock never happens.
- Why not “make the blink faster”? Waiting can't speed anything up. While loop() sits on that line, the blink isn't checked at all.
- Why not “read the button twice”? It reads the button over and over while it's held, but nothing else runs, so the blink stalls.
- Why not “do nothing”? It does something: it holds loop() on that line for as long as the button is down.
“Two jobs at once” is lesson 2 of 6 in Clockwork Cove, Embedded Systems World 2. Free with an account. Lessons unlock in order, so you’ll do the 8 before it first.
Questions people ask
Can an Arduino run two loops at the same time?
An Uno has one processor and runs one line at a time, so there are no true parallel loops. It doesn't need them: if no job waits, loop() comes round thousands of times a second and gives every job a quick turn.
Why does my button stop working while the LED blinks?
A delay() in the blink freezes the whole sketch, so the button is read only between waits, and a quick press can come and go unseen. Time the blink with millis() and read the button on every pass.
What is a state change in Arduino button code?
The pass where the pin reads differently from the pass before, like HIGH last time and LOW now. A press lasts thousands of passes but changes state only once, so acting on the change does something exactly once per press.
Related topics
- millis() vs delay() on ArduinoWhy delay() freezes your sketch, and how millis() runs several jobs at once.
- How to blink an LED without delay() on ArduinoBlink an LED with millis() so loop() stays free for every other job.
- How to debounce a button on ArduinoWhy one press counts as four, and the 50 ms fixes that make it count once.
- How to count button presses on an ArduinoCount the change, not the hold, keep the count above setup(), and use % for every Nth press.