Learning Goals 5 min
- Define blocking:
delay(N)halts the entire program for N milliseconds — nothing else runs during that time. - Reproduce three bugs that
delay()causes: laggy buttons, missed sensor events, and stuttering animations. Feel the pain firsthand. - Recognise the next two lessons (L02-34
millis(), L02-35 Blink Without Delay) as the systematic fix.
Warm-Up 10 min
For all of Level 1 and most of Level 2, we've used delay() liberally and gently tip-toed around its limits. It's simple, it works, it's in every example sketch. But you've already met situations where it broke things — the parking sensor that lagged because delay(beepInterval) stole the sensor read; the LCD that froze when the DHT was reading. Today is the lesson where we deliberately stare at delay()'s problems before, in the next two lessons, we replace it.
The one-sentence summary
delay(N) means "sit on this exact line and do nothing for N milliseconds". The Arduino CPU is fully occupied — it's not reading sensors, not responding to buttons, not stepping motors, not updating the LCD. Just waiting.
New Concept · What "blocking" really means 20 min
The three bugs delay() causes
- Button lag. Press a button while the sketch is mid-
delay— and your press isn't registered until the delay finishes. If the delay is 1 second, the button feels mushy. If it's 5 seconds, you wonder if the button is broken. - Missed events. A sensor pulse that lasted 20 ms passed during your 500 ms delay → you never saw it. For a knock sensor or a fast button this is fatal.
- Stuttering motion. Animating an LED brightness fade with
delay(10)between steps means anything else in your loop (sensor read, LCD refresh) hijacks the timing → some steps are 10 ms apart, some are 30 ms.
The metaphor
Imagine you're a librarian. Your boss says "every 5 minutes, ring the bell". You set a timer for 5 minutes, sit completely still doing nothing, watch the timer tick down. Meanwhile your phone rings, a student asks a question, the printer jams. You don't respond to any of them — your only job for 5 minutes was "wait".
That's your Arduino during delay(). Now imagine the smarter librarian: you write down "ring bell at 5:30", then carry on stamping books and answering questions. Every minute or so you glance at the clock. When it hits 5:30, you ring the bell. That's millis() — tomorrow's lesson.
What delay doesn't pause
Three Arduino features keep running even during delay(), because they're handled by hardware peripherals (not the CPU):
millis()— the millisecond counter — still advances. Same as your phone's clock: even while you're asleep, time still passes.- Interrupts (we'll meet them properly in L3) — these can pause the delay momentarily.
- Serial RX — incoming serial data is buffered by hardware; you'll catch up after the delay.
Everything else stops. analogRead, digitalWrite, your sensor reads, your LCD refresh, your button polling — all frozen.
Worked Example · The painful-button demo 25 min
Today is a deliberate "break it on purpose" lesson. We'll write a sketch that uses delay() badly, then physically demonstrate that the badness is real.
Step 1 — wiring
| Component | Pin |
|---|---|
| LED (with 220 Ω) | D9 |
| Button (with INPUT_PULLUP) | D2 |
Simple: one LED, one button. The button is wired between D2 and GND; we'll read it as digitalRead(2) == LOW when pressed.
Step 2 — the "bad" sketch
Save as delay-bad.ino:
// L02-33: Demonstrates how delay() ruins button responsiveness.
const int LED = 9;
const int BUTTON = 2;
bool ledState = LOW;
void setup() {
pinMode(LED, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
// Toggle the LED every 2 seconds
ledState = !ledState;
digitalWrite(LED, ledState);
// Check the button
if (digitalRead(BUTTON) == LOW) {
Serial.println("Button pressed!");
}
delay(2000); // ← the villain
}Step 3 — upload and try to press the button
Press the button quickly and release. Watch the Serial Monitor. You'll see "Button pressed!" only if you happened to hold the button at the exact instant the loop reached digitalRead — which happens once every 2 seconds.
Try this: press the button five times in five seconds. You'll get 2 or 3 prints if you're lucky, sometimes 0. The button feels broken. It isn't — the sketch is just ignoring it 99.9% of the time, blocked in delay(2000).
Step 4 — the "naive fix"
The temptation is to add the button check inside the delay:
for (int i = 0; i < 200; i++) {
if (digitalRead(BUTTON) == LOW) {
Serial.println("Button pressed!");
}
delay(10);
}Better — now we check every 10 ms instead of every 2000 ms. But this still has problems:
- One physical press will register as 20+ "pressed" events (state-change detection isn't there).
- What if we add a third thing to do? Now we need two nested polling loops. The complexity explodes.
- The LED toggle timing slips by however long the inner work takes.
Step 5 — the third bug: missed knock
Replace the button with a piezo knock sensor (from L02-18) on A0. The sketch now reads:
void loop() {
ledState = !ledState;
digitalWrite(LED, ledState);
if (analogRead(A0) > 100) Serial.println("KNOCK");
delay(2000);
}Tap the piezo 5 times in 5 seconds. You'll catch maybe 0–2 knocks. The piezo's spike lasts ~2 ms; the loop only samples once every 2000 ms. You miss virtually everything.
Step 6 — the takeaway
Three concrete bugs from one instance of delay(). In a real project with multiple sensors, multiple outputs, and a user interface, delay() is unworkable. The next two lessons replace it with the millis() pattern — same effect ("do X every N seconds") but without freezing everything else.
Try It Yourself 15 min
Goal: Modify the bad sketch so the LED blinks once per second instead of every two. Run it. Try the button. Is the lag any better? Quantify in your notebook.
Hint
The blink happens twice per loop (on then off, sort of — actually we're toggling once per delay). To get a 1 s blink rate, halve the delay to 1000.
delay(1000);The button still feels half as bad as before. The pattern: button lag is roughly delay length. To make buttons feel snappy, your delay can't be more than ~50 ms — and that's often incompatible with the timing you actually want.
Goal: Measure the actual time between blinks. Add Serial.println(millis()); at the top of loop(). Watch a few seconds' output. Compute the gap between consecutive lines. Is it exactly 2000 ms?
Hint
Expected output:
0 2003 4007 6011 8015
The gap is slightly more than 2000 because each loop also runs the digitalRead, the Serial.println, and a few other house-keeping operations — they add ~3 ms each iteration. delay(2000) is more like delay(2000 + everything else). With millis() scheduling tomorrow we can hit exactly 2000 ms gaps.
Goal: Add a buzzer chirp every time the LED toggles. Then add a sensor read (any analog sensor) on every loop iteration. Then add an LCD refresh that prints the sensor reading. Now run everything together. Measure how badly the "every 2 seconds" promise drifts as more work piles up before the delay.
Hint
By the time you have a sensor read (1 ms), a sprintf (1 ms), an LCD print (3 ms), a chirp (60 ms during tone), and a button check, you may already be ~70 ms late on each cycle. After an hour the LED has fallen 60+ seconds behind real time. millis() scheduling never drifts because it tracks absolute time, not "time since last delay".
Mini-Challenge · Audit your previous sketches 15 min
Today is the lesson where you turn detective on your own code. Pick three sketches you've written in L1 or L2 and find every delay() call. For each:
- Write down the delay length.
- Write down what the sketch is doing during that delay.
- Write down what the sketch can't do during that delay (button check, sensor read, LCD update).
- Rate the delay's harm on a scale: OK (short, no rival work), annoying (button feels mushy), or broken (sensor events get missed).
By the end you should have ~10 delays catalogued, with a rough idea of which sketches will benefit most from the L02-34 / L02-35 refactor. Keep the list — it's your refactor backlog for the next two lessons.
Recommended starting points:
- L02-12 personal thermometer (the 250 ms loop delay).
- L02-20 weather station v1 (the LOG_MS gate is already millis-based, but the inner sensor reads use blocking calls).
- L02-25 parking display (the 100 ms read cadence).
- L02-32 digital thermometer with LCD (the 2000 ms sample interval is millis-based; check the button polling).
Recap 5 min
delay(N) is the "freeze for N milliseconds" instruction. During the freeze, your Arduino does nothing — no sensors, no buttons, no LCD, no maths. For tiny pauses (under ~50 ms) it's usually fine; for the "every 2 seconds, take a reading" kind of work it kills responsiveness, misses fast events, and makes multi-task sketches impossible. The mental shift is from "sleep until time X" to "keep working but glance at the clock periodically". Tomorrow we meet millis() — the clock you glance at — and the day after we use it for the canonical Blink Without Delay.
- Blocking
- A function that doesn't return until its work is done.
delay()is blocking.analogRead()is mildly blocking (~100 µs).dht.readTemperature()is significantly blocking (~20 ms). - Non-blocking
- A function that does a small amount of work and returns immediately. The next call (some time later) does the next small piece. Non-blocking sketches use
millis()to schedule the work. delay(ms)- Pause execution for
msmilliseconds. Hardware peripherals (timers, serial RX, interrupts) keep running; everything else in your sketch stops. delayMicroseconds(us)- Same idea at a smaller scale. Still blocking — a 100 000 µs delay is just
delay(100)rephrased. - Button lag
- The user-visible symptom of polling a button only at the end of a long delay. Press → wait → eventually registered. Feels broken even when it isn't.
- Missed event
- A short-duration sensor signal (knock, button press, ultrasonic echo) that occurred during a blocking call and was therefore invisible to the sketch.
- Cumulative drift
- The slow timing error that accumulates in delay-based sketches because each "every 2 seconds" really means "2000 ms PLUS whatever other work the loop did".
millis()scheduling avoids this. - Hidden delays
- Blocking calls that aren't named
delay— library reads,lcd.clear(), EEPROM writes. They have the same effect; you have to learn which calls block.
Homework 5 min
Read the Blink Without Delay tutorial. Search "Blink Without Delay Arduino tutorial" — the official Arduino docs has an article titled exactly that. Read it (10 minutes), then write down on paper:
- What variable does it use to remember when the LED last toggled?
- What expression tells the sketch "has it been long enough?"
- How does the sketch know the LED's current state without a global flag?
- Why does this approach work for "do many things at once"?
You don't have to code anything tonight — we'll work through Blink Without Delay together over the next two lessons. The point is just to pre-load the pattern so it's familiar when we get there.
Also bring:
- Your delay-audit catalogue from the mini-challenge.
- Your four pre-reading answers.
- No sketch — tomorrow we write it together.