Learning Goals 5 min
- Translate the classic
delay(500)-based blink into themillis()-based "Blink Without Delay" — line by line, with no magic. - Introduce the state-tracking variable: a
boolthat remembers whether the LED is currently on or off, so the loop knows what to flip to next. - Use the same pattern to blink TWO LEDs at different rates from a single loop — proving that the pattern actually solves the "do many things at once" problem.
Warm-Up 10 min
Yesterday: millis() for "every N seconds, do something". That works when the "something" is a one-shot event — print a value, take a reading. Blinking is trickier because each cycle has two phases (on, off) of potentially different lengths. The classic Blink Without Delay sketch shows how to handle this elegantly.
Quick warm-up
You already know the delay version:
void loop() {
digitalWrite(LED, HIGH);
delay(500);
digitalWrite(LED, LOW);
delay(500);
}The pattern: turn on, wait, turn off, wait, repeat. The blockingness is obvious — the LED's timing dominates the loop. Today's rewrite eliminates the blocking.
New Concept · The non-blocking blink 25 min
The canonical sketch
Almost identical to the official Arduino "BlinkWithoutDelay" example. Save in your head:
const int LED = 13; // built-in LED
const long INTERVAL = 500; // ms between flips
int ledState = LOW;
unsigned long previousMillis = 0;
void setup() {
pinMode(LED, OUTPUT);
}
void loop() {
unsigned long currentMillis = millis();
if (currentMillis - previousMillis >= INTERVAL) {
previousMillis = currentMillis;
// flip the LED's state
ledState = (ledState == LOW) ? HIGH : LOW;
digitalWrite(LED, ledState);
}
}The three new pieces
int ledState— the "state-tracking variable". Without it the sketch can't know whether to turn the LED on or off this iteration. (The delay version "knew" via the strict order of statements; the non-blocking version needs an explicit flag.)unsigned long previousMillis— the timestamp of the last flip. Initialised to 0 so the first flip happens shortly after boot (when millis() - 0 first exceeds INTERVAL).- The ternary flip
ledState = (ledState == LOW) ? HIGH : LOW;— concise "toggle" without an if-else. Equivalent toledState = !ledStateon most boards (since LOW is 0 and HIGH is 1) but reads more explicitly.
Why this is "the" pattern
Three reasons it's the canonical Arduino example:
- It's the smallest sketch that exhibits all three of: a state variable, a timestamp, and a periodic event.
- It generalises to any number of LEDs — just add another
state+previouspair. - It generalises to any number of timed actions — every "something every N ms" in your sketch fits this shape.
The two-LED extension
const int LED1 = 9;
const int LED2 = 10;
const long INTERVAL1 = 500;
const long INTERVAL2 = 350;
int led1State = LOW, led2State = LOW;
unsigned long previous1 = 0, previous2 = 0;
void setup() {
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
}
void loop() {
unsigned long now = millis();
if (now - previous1 >= INTERVAL1) {
previous1 = now;
led1State = !led1State;
digitalWrite(LED1, led1State);
}
if (now - previous2 >= INTERVAL2) {
previous2 = now;
led2State = !led2State;
digitalWrite(LED2, led2State);
}
}Two LEDs blinking at different rates from the same loop. The patterns drift in and out of sync naturally — they're independent. No nesting, no conflict, no interference. Add a third LED? Add a third pair of variables and a third if-block. Linear scaling.
The deeper insight
What just happened is that we replaced "time as control flow" (the blocking delay sequence) with "time as data" (the timestamp). The loop now runs at full speed, all the time; each "event" is gated by reading the clock. This is the same model used by all serious embedded systems — RTOSes, FreeRTOS, microPython's scheduler. We're using a tiny manual version, but the philosophy scales up directly.
Worked Example · Side-by-side comparison 20 min
Today is a comparison lesson. We're going to wire two LEDs and run two sketches — one delay-based, one millis()-based — and feel the difference in your hands.
Step 1 — wiring
| Component | Pin |
|---|---|
| LED 1 (with 220 Ω) | D9 |
| LED 2 (with 220 Ω) | D10 |
| Button (INPUT_PULLUP) | D2 |
Step 2 — the blocking version
Save as blink-delay.ino. Try to use the button while it's running:
const int LED1 = 9, LED2 = 10, BUTTON = 2;
int pressCount = 0;
void setup() {
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
digitalWrite(LED1, HIGH);
delay(500);
digitalWrite(LED1, LOW);
delay(500);
digitalWrite(LED2, HIGH);
delay(350);
digitalWrite(LED2, LOW);
delay(350);
if (digitalRead(BUTTON) == LOW) {
pressCount++;
Serial.print("Press count: ");
Serial.println(pressCount);
}
}Upload. The two LEDs blink, but their timing is wrong — LED2 only blinks during the gaps in LED1's timing. The button is essentially unusable; press it 10 times in 5 seconds and you might catch 1 or 2 presses.
Step 3 — the non-blocking version
Save as blink-no-delay.ino:
const int LED1 = 9, LED2 = 10, BUTTON = 2;
const long INTERVAL1 = 500, INTERVAL2 = 350;
int led1 = LOW, led2 = LOW;
unsigned long prev1 = 0, prev2 = 0;
int pressCount = 0;
bool lastButton = HIGH;
void setup() {
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
pinMode(BUTTON, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
unsigned long now = millis();
if (now - prev1 >= INTERVAL1) {
prev1 = now;
led1 = !led1;
digitalWrite(LED1, led1);
}
if (now - prev2 >= INTERVAL2) {
prev2 = now;
led2 = !led2;
digitalWrite(LED2, led2);
}
// Button (state-change detection — count each press once)
bool b = digitalRead(BUTTON);
if (lastButton == HIGH && b == LOW) {
pressCount++;
Serial.print("Press count: ");
Serial.println(pressCount);
}
lastButton = b;
}Step 4 — compare them physically
- Both versions blink the LEDs.
- In the delay version, LED1 and LED2 take turns — they can't blink at "independent" rates.
- In the millis() version, the two LEDs blink at truly independent rates, drifting in and out of phase naturally.
- In the delay version, the button has to be held perfectly for ~2 seconds to register.
- In the millis() version, every press registers instantly. You can press the button 50 times in 10 seconds and the count goes up by 50.
Step 5 — feel the difference
Don't just observe — actually try to press the button quickly five times in a row on both versions. Note how mushy and unresponsive the delay version feels vs how crisp the millis() version feels. That feeling is the upgrade Cluster F brings to every project from here on.
Basic 7 min
Goal: Make LED1 blink with an asymmetric pattern: 100 ms on, 900 ms off (a heartbeat blink). Same Blink Without Delay structure, but the interval changes based on the current state.
Challenge 1 7 min
Goal: Use the button (from step 4) to change LED1's blink interval. Each press cycles through 200 ms → 500 ms → 1000 ms → 2000 ms → back to 200 ms.
Challenge 2 6 min
Goal: Build a three-LED chase: LEDs on D9, D10, D11 light up one at a time in sequence, 200 ms each, then start over. Same Blink Without Delay shape but with three states.
Challenge 3 · The traffic-light + crossing — non-blocking 15 min
Remember L01-46's traffic light + pedestrian crossing? Build it again — but this time, with full Cluster F discipline: no delay() anywhere.
The brief:
- Three car LEDs (red, amber, green) cycling on a schedule: green 6 s → amber 2 s → red 6 s → red+amber 1 s → repeat.
- A pedestrian button that requests the next safe stop. When pressed, the cycle finishes its current green-then-amber, then holds at red for the full red period before continuing.
- The pedestrian red/green LEDs (or just a buzzer) signal "walk" / "don't walk" correctly throughout.
- The button is responsive — pressing during green should be acknowledged immediately (maybe with a confirmation tone), even though the cars don't actually turn red until the cycle completes.
It's done when:
- Pressing the button during green: confirmation tone immediately; cars finish green-amber-red sequence normally; pedestrian gets to walk during the red.
- Pressing during red: pedestrian gets to walk immediately for the remainder of the red.
- Zero
delay()calls in the sketch. - You can press the button 10 times in 3 seconds and it never misses one.
This is the most ambitious thing you've built so far — three LEDs, a button, a state machine, multiple millis() timers. If you can ship this, you've absorbed Cluster F.
Recap 5 min
Blink Without Delay is the canonical Arduino "graduate" sketch. It introduces three ideas you'll use forever: the state-tracking variable, the timestamp + interval pair, and the loop-spins-at-full-speed mindset. Combined with yesterday's millis() introduction, you can now do anything you used to do with delay() — but without the blocking, the lag, the missed events. Tomorrow we explicitly demonstrate "doing two things at once" with two LEDs and a clear case study; the day after we apply the pattern to a clean millis()-based debouncer.
- Blink Without Delay
- The classic Arduino example sketch that demonstrates the
millis()pattern with the simplest possible application: blinking an LED. Located in File → Examples → Digital → BlinkWithoutDelay in the IDE. - State-tracking variable
- A variable (here
ledState) that remembers what the device is currently doing, so the loop can decide what to do next without re-deriving it from scratch. - Toggle (
!) - The logical-NOT operator.
!HIGH= LOW,!LOW= HIGH. Used to flip a boolean / pin state in one character. - Ternary operator (
?:) - The one-line if-else:
condition ? valueIfTrue : valueIfFalse. Useful for picking between two values inline. - Per-thing state
- The discipline of giving each "thing" its own state and timestamp pair. Two LEDs = two pairs. Avoid the temptation to share variables — it leads to coupled bugs.
- Time as data
- The big shift from delay-based to millis-based code. In delay-based, time is in the call sequence (blocking). In millis-based, time is a value (the timestamp) that you do arithmetic with.
- Linear scaling
- The property that adding a third (or tenth) timed action only requires copy-pasting the timer pattern — no restructuring.
delay-based sketches can't do this;millis()-based sketches can.
Extra Mission 5 min
Part 1 — Design a signal with several lights
Pick a real signal that uses blinking lights. Ideas: a level crossing, a lighthouse, a bike rear light, a runway. On paper, design your own version with two or three LEDs.
Your design must include:
- What each LED means.
- Each LED's on time and off time, in ms. They may be different.
- At least two modes, and how a button switches between them.
- A variable plan: one state, one previous time and one interval per LED.
Part 2 — Make it
Build it with one Blink Without Delay block per LED, plus a button that changes the mode. Use zero delay() calls. Check that changing mode never makes an LED freeze or skip a beat.
Bring back next class: your design, your hw-l02-35.ino sketch, and a short video showing both modes.