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.
Try It Yourself 20 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.
Hint
if (now - prev1 >= (led1 == HIGH ? 100 : 900)) {
prev1 = now;
led1 = !led1;
digitalWrite(LED1, led1);
}One line ternary picks the right interval based on the current state. Now you have asymmetric blinking with no extra variables. Real heartbeat LEDs (status indicators on routers, IoT devices) usually use this pattern.
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.
Hint
const long INTERVALS[] = {200, 500, 1000, 2000};
int intervalIdx = 0;
// in the button-press branch:
intervalIdx = (intervalIdx + 1) % 4;
Serial.print("Now blinking at "); Serial.println(INTERVALS[intervalIdx]);
// in the LED1 check:
if (now - prev1 >= INTERVALS[intervalIdx]) { ... }Demonstrates that the interval doesn't have to be a compile-time constant — it can come from a sensor, a button, an LCD menu, anything.
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.
Hint
This needs a single timer and an index variable that cycles 0, 1, 2, 0, 1, 2, ...
const int PINS[] = {9, 10, 11};
const int N = 3;
int current = 0;
unsigned long prev = 0;
const long STEP_MS = 200;
void loop() {
unsigned long now = millis();
if (now - prev >= STEP_MS) {
prev = now;
for (int i = 0; i < N; i++) digitalWrite(PINS[i], LOW);
current = (current + 1) % N;
digitalWrite(PINS[current], HIGH);
}
}The for-loop ensures only one LED is ever on. Same Knight Rider effect as L01-11 but now running alongside any number of other things in the same loop.
Mini-Challenge · 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.
Homework 5 min
Two-stage LED puzzle. Build a sketch with:
- LED on D9 blinking 1 second on / 1 second off (the "clock" LED).
- LED on D10 that blinks at a rate of your choice between 0.1 s and 2 s.
- A button on D2 that, when pressed, swaps the two LEDs' rates. So pressing the button transfers D10's rate to D9 and vice versa.
Constraints:
- Zero
delay()calls. - Button presses are detected immediately, not lagged.
- When the rates swap, neither LED skips a beat — they continue from where they were, just at the new rate.
On paper, draw a small diagram showing your variables: which state, which previous, which INTERVAL per LED. Use the diagram to plan before you code.
Bring back next class:
- Your variable diagram.
- Your
hw-l02-35.inosketch. - A note: when you tested the swap, did either LED skip a beat? If yes, why? Did you fix it?