28. Non-Blocking Timing with millis()
Run several timed tasks together without delay().
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
- Use unsigned long for every millis timestamp
Parts and preparation
Uno, two LEDs with 330 ohm resistors, one push-button and jumpers.
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(); }
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 08 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.
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 .ino sketchconst 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.
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.
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.