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.
Basic 5 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.
Challenge 1 5 min
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?
Challenge 2 5 min
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.
Challenge 3 · Find the blind spot 15 min
This alarm should flash and beep every 2 seconds. Pressing the button on D2 should print Silence!. Users say the button "only works sometimes".
const int LED = 9;
const int BUZZER = 8;
const int BUTTON = 2;
void setup() {
pinMode(LED, OUTPUT);
pinMode(BUZZER, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
digitalWrite(LED, HIGH);
tone(BUZZER, 880);
delay(300);
digitalWrite(LED, LOW);
noTone(BUZZER);
delay(700);
if (digitalRead(BUTTON) == LOW) {
Serial.println("Silence!");
}
delay(1000);
}Work these out on paper first:
- How long one trip through
loop()takes. - How many times per trip the button is checked.
- A quick tap lasts about 150 ms. Roughly what fraction of quick taps will be caught?
Now build it: LED on D9 with a 220 Ω resistor, buzzer on D8, button from D2 to GND. Tap the button 10 times and count the prints. Does the count match your answer to question 3?
Finally, use the naive fix from Step 4. Write a helper waitAndWatch(int ms) that waits in 10 ms steps and checks the button at every step. Replace all three delay() calls with it.
It works if the flash-and-beep pattern looks the same as before, and 10 quick taps give at least 10 Silence! lines.
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.
Extra Mission 5 min
Part 1 — Design a gadget that must never miss a press
Pick an everyday gadget where a missed press would annoy someone. Ideas: a quiz buzzer, a doorbell, a game controller, a lift call button. On paper, design it as an Arduino project.
Your design must include:
- A name and a one-sentence job.
- Every timed thing it does, such as flashing or beeping, and how often.
- Every input it must never miss, and the longest wait a user would accept, in ms.
- Its worst-case button lag if every timed thing used
delay(). Is that acceptable?
Part 2 — Make it
Build the core of your gadget: one button plus your timed outputs. Use the waitAndWatch() idea from Challenge 3 so the button is checked every 10 ms. Count presses with a variable, but only when the button changes from HIGH to LOW. Press 20 times and compare with the printed count.
Bring back next class: your design with its lag sum, your hw-l02-33.ino sketch, and a Serial Monitor screenshot after 20 presses.