Lesson 6 / 25
Non-blocking Timing with millis()
Why delay() hurts and how to avoid it.
The blocking problem
delay(ms) busy-waits: during that time loop() cannot read buttons, process serial data or update other outputs. One delay is fine for Blink, but a project with a blinking LED, a button and a sensor quickly becomes unresponsive. The fix is non-blocking timing: millis() returns milliseconds since reset as an unsigned long. Each task remembers when it last ran and checks, on every pass of loop(), whether its interval has elapsed. Writing the check as millis() - last >= interval with unsigned arithmetic stays correct even when the counter wraps around (after roughly 49.7 days on a 32-bit counter).
Two LEDs at independent rates
No delay(): loop() stays free for other work.
const uint8_t LED_A = 8, LED_B = 9;
unsigned long lastA = 0, lastB = 0;
bool stateA = false, stateB = false;
void setup() {
pinMode(LED_A, OUTPUT);
pinMode(LED_B, OUTPUT);
}
void loop() {
unsigned long now = millis();
if (now - lastA >= 500) { // every 500 ms
lastA = now;
stateA = !stateA;
digitalWrite(LED_A, stateA);
}
if (now - lastB >= 120) { // every 120 ms
lastB = now;
stateB = !stateB;
digitalWrite(LED_B, stateB);
}
// read buttons, sensors, serial here without being blocked
}Subtract, never add
Write now - last >= interval, not now >= last + interval. The addition form breaks when millis() wraps; the subtraction form works because unsigned arithmetic wraps too.
Quick check: Why is millis()-based timing preferred over delay() in larger sketches?
- It disables interrupts for accuracy
- It makes the clock run faster
- It uses less flash memory than any delay
- It lets loop() keep running other tasks instead of busy-waiting
Answer
It lets loop() keep running other tasks instead of busy-waiting — Cooperative, non-blocking checks keep the program responsive.