11. Non-Blocking Timing & State Machines
Run several timed tasks together without delay(), debounce a button alongside them, and sequence timed steps with a state machine.
Learning outcomes
- Explain why delay blocks other work in loop
- Apply the millis pattern: remember, compare, act, stamp
- Run independent timers for two LEDs at different rates
- Debounce a button with millis without calling delay
- Sequence timed steps with a state variable (a state machine)
- Use unsigned long for every millis timestamp
Parts and preparation
Uno, two LEDs with 330 ohm resistors, one push-button and jumpers. For the state machine example: red, amber and green LEDs with 330 ohm resistors.
Before power: inspect wiring, confirm supply voltage and ensure all connected circuits share GND.
Why delay blocks
delay(ms) freezes the whole sketch until the wait ends. During that freeze, loop cannot read buttons, update a second LED, print Serial, or service sensors.
millis() returns milliseconds since the board started and keeps counting while your code runs. Instead of sleeping, you check whether enough time has passed since the last action for each task.
The millis pattern
For each timed task: store the last time you acted, compare against the interval, act when due, then update the stamp.
Pseudocode: if (now - lastRun >= interval) { lastRun = now; doWork(); }
Code Along writes lastRun += interval; instead of lastRun = now;. Both work - += keeps the ticks exactly one interval apart even when loop() runs a little late.
unsigned long now = millis();
if (now - lastFast >= 200) {
lastFast = now;
fastState = !fastState;
digitalWrite(ledFast, fastState);
}Independent timers
Each task needs its own lastRun variable and its own interval. The worked sketch blinks D5 every 200 ms and D6 every 700 ms while still watching a button.
Tasks share loop but do not wait for each other. If one LED rate looks wrong, check that task's interval and stamp - not the other task.
Shared timeline
Imagine millis as a shared clock. Fast toggles happen often, slow toggles less often, and the button is checked on every loop pass. Empty gaps are not delay - loop is still spinning and checking.
Debounce without delay
Mechanical buttons bounce. Lesson 10 used a millis debounce; here it sits beside the LED timers.
When the raw reading changes, restart lastDebounce. Only after about 40 ms of quiet, and only if the reading differs from the last stable state, accept the new edge. On a stable press to GND (LOW with INPUT_PULLUP), increment the press count.
State machines: one variable, many steps
A state machine stores which step a behaviour is in, in one variable. Each pass of loop() asks two questions: which state am I in, and is it time to leave it? Each state can last a different time and switch different outputs, which a plain blink cannot do.
Write the states as a table first - state, outputs, how long, next state - then turn each row into one branch. Record stateStart whenever the state changes, so elapsed time is measured from the start of the current step. The same shape runs alarm sequences, motor start-up and the practical assessment tasks.
| State | Outputs | Lasts | Next state |
|---|---|---|---|
| RED | Red on | 4 s | GREEN |
| GREEN | Green on | 3 s | AMBER |
| AMBER | Amber on | 1 s | RED |
unsigned long elapsed = millis() - stateStart;
if (state == RED && elapsed >= 4000) {
state = GREEN;
stateStart = millis();
showLights(false, false, true);
} else if (state == GREEN && elapsed >= 3000) {
state = AMBER;
stateStart = millis();
showLights(false, true, false);
} else if (state == AMBER && elapsed >= 1000) {
state = RED;
stateStart = millis();
showLights(true, false, false);
}unsigned long and overflow
millis() returns unsigned long. Store every timestamp as unsigned long too. Using int will break as time grows.
After about 49 days millis rolls over to zero. The subtraction now - lastRun still works correctly with unsigned long arithmetic, so do not rewrite the pattern with signed types or absolute millis comparisons.
| Item | Course practice |
|---|---|
| Clock | unsigned long now = millis(); |
| Each task stamp | its own unsigned long lastX |
| Compare | now - lastX >= interval |
| Update | lastX = now after the action |
Wiring and safe build sequence
- D5 -> 330 ohm -> LED1 -> GND
- D6 -> 330 ohm -> LED2 -> GND
- Button from D2 to GND; use INPUT_PULLUP
Worked sketch
Download .inoconst byte ledFast = 5;
const byte ledSlow = 6;
const byte buttonPin = 2;
unsigned long lastFast = 0;
unsigned long lastSlow = 0;
unsigned long lastDebounce = 0;
bool fastState = false;
bool slowState = false;
bool lastStable = HIGH;
bool lastReading = HIGH;
unsigned int pressCount = 0;
void setup() {
pinMode(ledFast, OUTPUT);
pinMode(ledSlow, OUTPUT);
pinMode(buttonPin, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
if (now - lastFast >= 200) {
lastFast = now;
fastState = !fastState;
digitalWrite(ledFast, fastState);
}
if (now - lastSlow >= 700) {
lastSlow = now;
slowState = !slowState;
digitalWrite(ledSlow, slowState);
}
bool reading = digitalRead(buttonPin);
if (reading != lastReading) {
lastDebounce = now;
lastReading = reading;
}
if ((now - lastDebounce) >= 40 && reading != lastStable) {
lastStable = reading;
if (lastStable == LOW) {
pressCount++;
Serial.print("Presses: ");
Serial.println(pressCount);
}
}
}How the code works
- Each LED has its own timer so the rates stay independent.
- The debounce window is 40 ms and does not call delay.
- Use unsigned long for every millis timestamp.
- INPUT_PULLUP: open reads HIGH, pressed to GND reads LOW.
Example 2: traffic light state machine
Download .inoWhat this sketch is for: Red, green and amber LEDs step through a sequence with a different time for each state. loop() never waits, so a button or sensor could be added without changing the timing.
const byte redPin = 8;
const byte amberPin = 9;
const byte greenPin = 10;
const byte RED = 0;
const byte GREEN = 1;
const byte AMBER = 2;
byte state = RED;
unsigned long stateStart = 0;
void showLights(bool red, bool amber, bool green) {
digitalWrite(redPin, red);
digitalWrite(amberPin, amber);
digitalWrite(greenPin, green);
}
void setup() {
pinMode(redPin, OUTPUT);
pinMode(amberPin, OUTPUT);
pinMode(greenPin, OUTPUT);
Serial.begin(9600);
showLights(true, false, false);
Serial.println("RED");
}
void loop() {
unsigned long elapsed = millis() - stateStart;
if (state == RED && elapsed >= 4000) {
state = GREEN;
stateStart = millis();
showLights(false, false, true);
Serial.println("GREEN");
} else if (state == GREEN && elapsed >= 3000) {
state = AMBER;
stateStart = millis();
showLights(false, true, false);
Serial.println("AMBER");
} else if (state == AMBER && elapsed >= 1000) {
state = RED;
stateStart = millis();
showLights(true, false, false);
Serial.println("RED");
}
}How the code works
- Named constants RED, GREEN and AMBER make the state variable readable.
- Each branch checks its own state and its own duration, then records stateStart for the next step.
- showLights() sets all three outputs at once, so no state can leave a stray LED on.
- Wiring: D8, D9 and D10 each through 330 ohm to the red, amber and green LED anodes; cathodes to GND.
Test and record evidence
Practical evidence checklist
Common faults and checks
- If both LEDs freeze when the button is held, a delay was introduced somewhere.
- Missed presses usually mean the debounce time is too long or the button wiring is wrong.
- Wrong blink rate: check that task's interval and that lastX is updated only when the action runs.
- Confirm LED polarity and 330 ohm series resistors on D5 and D6.
- Button: pinMode INPUT_PULLUP and switch to GND only.
- State machine skips a state or races through: stateStart is not being updated when the state changes.
Check your understanding
Q1. Why avoid long delay calls?
Show answer
They stop loop from servicing other tasks.
Q2. Why use unsigned long with millis?
Show answer
millis returns unsigned long; correct overflow math needs matching types.
Q3. What does each timed task need?
Show answer
Its own lastRun stamp and its own interval.
Q4. When do you update lastFast?
Show answer
After the fast LED action runs, set lastFast = now.
Q5. What does the state variable in a state machine store?
Show answer
Which step of the sequence is running now.
Practise in your browser
Write and test this lesson's code on a virtual Uno. Work through the 6 exercises in order. Press Test and each one checks itself.
Why delay() gets in the way
delay(500) stops the whole sketch for half a second. While it waits the Uno cannot read a button, update a display or blink a second LED. With one LED that is fine; with two tasks it breaks.
The fix is to check the clock instead of waiting. millis() returns how many milliseconds have passed since the board started. Each task remembers when it last ran and acts again when enough time has passed:
if (millis() - lastRun >= interval) { lastRun += interval; ... }
loop() then runs thousands of times a second, and each task only does its work when it is due.
The lesson writes lastRun = now; instead of lastRun += interval;. Both work: += keeps the ticks exactly interval apart even if loop() is a little late, which is why this chapter uses it.
Blink without delay
This is the Lesson 11 pattern. Watch the loop() counter on the virtual Uno: it runs thousands of times while the LED waits, so there is plenty of time for other work.
Blink every 500 ms with millis()
Toggle the onboard LED every 500 ms using millis() - no delay() allowed. The variables you need are already declared.
Heartbeat message
Print alive to the Serial Monitor once every second, using the same millis() pattern - no delay(). A heartbeat like this tells you a long-running sketch has not frozen.
alive alive alive
Two LEDs, two speeds
The red LED on pin 9 already blinks every 300 ms. Add a second, independent timer so the green LED on pin 10 blinks every 700 ms. With delay() this is almost impossible; with two millis() timers it is straightforward.
A button that answers instantly
This sketch blinks the onboard LED and lights the green LED on pin 8 while the button on pin 2 is held. Press Run and hold the button: the green LED reacts late, because delay() blocks the button check.
Rewrite the blinking with millis() so the green LED follows the button within a few milliseconds. The test presses at 1230 ms and releases at 1800 ms.
State machines
A state machine remembers which step it is in with a variable, and moves to the next step when a condition is met - here, when enough time has passed. Each step can last a different length of time, which a plain blink cannot do. It is how Lesson 11 builds a traffic light without delay(), and how real controllers keep many tasks responsive.
On for 2 s, off for 1 s
Make the onboard LED stay on for 2 seconds and off for 1 second, without delay(). state remembers which half you are in: 0 is on, 1 is off.
Use the lesson's shape: one if / else if branch per state. Each branch checks its state and how long it has lasted, then switches state, sets stateStart = millis(); and writes the LED.
Traffic light state machine
Now three states. Run a traffic light on pins 10 (green), 11 (yellow) and 12 (red): green for 3 s, yellow for 1 s, red for 3 s, then back to green. showState() already switches the right LED on.
Same shape as the last exercise, with three branches. The state after 2 (red) is 0 (green) again.
Try it on a real Uno
Download Two LEDs, two speeds, wire two LEDs with 330 Ω resistors on pins 9 and 10, and upload it. Then add a button on pin 2 with INPUT_PULLUP and make it change one of the intervals while the LEDs keep blinking - something delay() could never do.
Want more? Code Along chapter 8 has a Deeper exercise that goes further than this lesson. It is optional.