Learning Goals 5 min
- Build the Cluster F capstone: three separate LED patterns running at three different speeds in one loop, with a button to swap between "shows".
- Apply tick functions (L02-36), debouncing (L02-37), and per-pattern state to a non-trivial real-time animation system.
- Refactor the result into a clean "pattern" abstraction — each pattern is a function that knows how to advance itself by one frame given the current time.
Warm-Up 10 min
Cluster F started with delay-is-bad and ends with this: a small piece of visual art that you couldn't have built before the cluster. Three LEDs, three completely independent animations, all running in real time off one loop. A button switches between three "shows". Total sketch size: under 150 lines. Total delay() calls: zero.
Three patterns to plan
For each LED we'll define a pattern function. Examples:
- Blink: on for X ms, off for Y ms, repeat.
- Breathe: PWM fade up over X ms, fade down over Y ms.
- Heartbeat: 100 ms on, 100 ms off, 100 ms on, 700 ms off (the classic two-thump pattern).
- Pulse-train: three quick flashes, long pause, repeat.
- Strobe: very fast on/off (30 ms / 30 ms) — like a club light.
New Concept · One pattern, many instances 20 min
The shape of a pattern function
Each pattern is "a function that you call once per loop iteration, that updates an LED's state based on the current time". We'll standardise the signature:
void patternBlink(int pin, unsigned long onMs, unsigned long offMs);
void patternBreathe(int pin, unsigned long periodMs);
void patternHeartbeat(int pin);Each function reads millis() and decides what the LED should be doing right now. None of them store any state — they compute everything from the current time. That makes them stateless and trivial to compose.
Stateless blink
void patternBlink(int pin, unsigned long onMs, unsigned long offMs) {
unsigned long period = onMs + offMs;
unsigned long phase = millis() % period;
digitalWrite(pin, phase < onMs ? HIGH : LOW);
}One line of arithmetic. The current phase within the period decides on/off. No previousMillis, no ledState. Re-entrant: call it as often as you like; the LED state is recomputed from millis() every time.
Stateless breathe
void patternBreathe(int pin, unsigned long periodMs) {
unsigned long phase = millis() % periodMs;
int half = periodMs / 2;
int brightness;
if (phase < (unsigned long)half) {
brightness = map(phase, 0, half, 0, 255); // fading up
} else {
brightness = map(phase, half, periodMs, 255, 0); // fading down
}
analogWrite(pin, brightness);
}A triangle wave: up for half the period, down for half. Same pattern, no state. Plug it on any PWM pin.
Stateless heartbeat
The two-thump cardiac pattern: 100 ms on, 100 ms off, 100 ms on, 700 ms off (total period 1000 ms). Four phases, look up which one we're in:
void patternHeartbeat(int pin) {
unsigned long phase = millis() % 1000;
bool on = (phase < 100) || (phase >= 200 && phase < 300);
digitalWrite(pin, on ? HIGH : LOW);
}Tweak the four numbers for different cardiac rhythms.
Composing them in a "show"
A show is just a set of three pattern calls. Switching shows is switching which function gets called:
enum Show { SHOW_BASIC, SHOW_CHASE, SHOW_PARTY };
Show show = SHOW_BASIC;
void runCurrentShow() {
switch (show) {
case SHOW_BASIC:
patternBlink(LED1, 500, 500);
patternBreathe(LED2, 2000);
patternHeartbeat(LED3);
break;
case SHOW_CHASE:
patternBlink(LED1, 100, 200);
patternBlink(LED2, 100, 200); // intentionally same as LED1 → in sync
patternBlink(LED3, 100, 200);
break;
case SHOW_PARTY:
patternBlink(LED1, 50, 50);
patternBreathe(LED2, 500);
patternBlink(LED3, 80, 120);
break;
}
}One function per show. The whole show changes when you flip an enum. No tear-down, no state reset, no race conditions — because the patterns are stateless.
Worked Example · Three patterns, three shows 25 min
Step 1 — wiring
| Component | Pin |
|---|---|
| LED1 (with 220 Ω) | D9 (PWM) |
| LED2 (with 220 Ω) | D10 (PWM) |
| LED3 (with 220 Ω) | D11 (PWM) |
| Show-cycle button | D2 (INPUT_PULLUP, to GND) |
Step 2 — the sketch
Save as multi-led-choreography.ino:
// L02-38: Multi-LED Choreography — Cluster F capstone
// Three LEDs running independent patterns. Button cycles between 3 shows.
const int LED1 = 9, LED2 = 10, LED3 = 11;
const int BUTTON = 2;
enum Show { SHOW_BASIC, SHOW_CHASE, SHOW_PARTY };
Show show = SHOW_BASIC;
const char* showName[] = {"BASIC", "CHASE", "PARTY"};
// ---- Stateless patterns ----
void patternBlink(int pin, unsigned long onMs, unsigned long offMs) {
unsigned long period = onMs + offMs;
unsigned long phase = millis() % period;
digitalWrite(pin, phase < onMs ? HIGH : LOW);
}
void patternBreathe(int pin, unsigned long periodMs) {
unsigned long phase = millis() % periodMs;
int half = periodMs / 2;
int brightness = (phase < (unsigned long)half)
? map(phase, 0, half, 0, 255)
: map(phase, half, periodMs, 255, 0);
analogWrite(pin, brightness);
}
void patternHeartbeat(int pin) {
unsigned long phase = millis() % 1000;
bool on = (phase < 100) || (phase >= 200 && phase < 300);
digitalWrite(pin, on ? HIGH : LOW);
}
void patternChase(int pin, int slot, int totalSlots, unsigned long stepMs) {
unsigned long currentSlot = (millis() / stepMs) % totalSlots;
digitalWrite(pin, currentSlot == (unsigned long)slot ? HIGH : LOW);
}
// ---- Show dispatch ----
void runShow() {
switch (show) {
case SHOW_BASIC:
patternBlink(LED1, 500, 500);
patternBreathe(LED2, 2000);
patternHeartbeat(LED3);
break;
case SHOW_CHASE:
patternChase(LED1, 0, 3, 150);
patternChase(LED2, 1, 3, 150);
patternChase(LED3, 2, 3, 150);
break;
case SHOW_PARTY:
patternBlink(LED1, 50, 80);
patternBreathe(LED2, 400);
patternBlink(LED3, 120, 60);
break;
}
}
// ---- Debounced button ----
struct Button {
int pin;
unsigned long lastChange;
int rawState, stableState, prevStable;
bool justPressed;
void begin(int p) {
pin = p;
pinMode(pin, INPUT_PULLUP);
rawState = stableState = prevStable = digitalRead(pin);
lastChange = millis();
justPressed = false;
}
void tick() {
int now = digitalRead(pin);
if (now != rawState) { rawState = now; lastChange = millis(); }
justPressed = false;
if (millis() - lastChange >= 50 && rawState != stableState) {
prevStable = stableState;
stableState = rawState;
if (stableState == LOW && prevStable == HIGH) justPressed = true;
}
}
};
Button btn;
void setup() {
pinMode(LED1, OUTPUT);
pinMode(LED2, OUTPUT);
pinMode(LED3, OUTPUT);
btn.begin(BUTTON);
Serial.begin(9600);
Serial.print("Show: "); Serial.println(showName[show]);
}
void loop() {
btn.tick();
if (btn.justPressed) {
show = (Show)((show + 1) % 3);
// Clear the LEDs before the next show — fast wipe avoids inheriting
// a fade or a HIGH from the previous show.
analogWrite(LED1, 0);
analogWrite(LED2, 0);
analogWrite(LED3, 0);
Serial.print("Show: "); Serial.println(showName[show]);
}
runShow();
}Step 3 — upload, watch the BASIC show
LED1 blinks at 1 Hz (500/500). LED2 breathes slowly (2 s up + down). LED3 does the heartbeat (two thumps per second). All three running together. Look at it. This is what you couldn't build cleanly before Cluster F.
Step 4 — press the button → CHASE
All three LEDs now light up in sequence — LED1 for 150 ms, then LED2 for 150 ms, then LED3 for 150 ms, repeating. The classic three-LED chase, executed by three independent patterns that happen to share the same step rate and totalSlots. Not coupled — just coincidentally synchronised.
Step 5 — press → PARTY
Strobe + fast breathe + asymmetric strobe. Looks like a tiny dance floor. Total chaos but controlled.
Step 6 — press → back to BASIC
The cycle closes. Note how snappy the button is — press while in a fast strobe and the show changes immediately, even though the loop is busy running patterns.
Try It Yourself 20 min
Goal: Add a fourth show — "OFF". All three LEDs off. Useful as the "sleep" mode.
Hint
case SHOW_OFF:
analogWrite(LED1, 0);
analogWrite(LED2, 0);
analogWrite(LED3, 0);
break;Add SHOW_OFF to the enum, the name array, and bump the modulus in the button handler from 3 to 4.
Goal: Design a fifth pattern of your own. Possibilities: random twinkle (LED on/off randomly with a long bias toward off); sine wave (smoother than the triangle breathe); Morse pulse (long-short-short-... sequence for a chosen letter).
Hint
Stateless sine wave:
void patternSine(int pin, unsigned long periodMs) {
unsigned long phase = millis() % periodMs;
float radians = (2 * PI * phase) / periodMs;
int brightness = (sin(radians) + 1) * 127;
analogWrite(pin, brightness);
}The +1 shifts the sine's -1..1 range to 0..2; the * 127 scales to 0..255 (well, 0..254 — round if you want exact 255).
Goal: Add a crossfade between shows. Instead of instantly switching, gradually fade out the old show's LEDs while fading in the new show over 1 second.
Hint
This is harder because crossfading requires combining the two shows' output. One approach:
Show prev = SHOW_BASIC;
unsigned long transitionStart = 0;
bool transitioning = false;
// in button-press handler:
prev = show;
show = (Show)((show + 1) % 3);
transitionStart = millis();
transitioning = true;
// in loop, after btn.tick():
if (transitioning) {
unsigned long el = millis() - transitionStart;
if (el >= 1000) transitioning = false;
else {
// Run BOTH shows but with reduced PWM:
int alpha = map(el, 0, 1000, 255, 0); // old → 0
// ... mix old and new ...
}
}The fully clean version requires patterns to produce a brightness value rather than write the pin directly. Refactor cost is real. Worth doing once for the experience.
Mini-Challenge · Build your own choreography 15 min
The mini-challenge today is creative: design TWO original shows that aren't in the example sketch. Each show must use all three LEDs and at least one of: blink, breathe, heartbeat, chase, your custom pattern.
Themes to spark ideas:
- "Traffic" — LED1 = red (blinks 1 s), LED2 = amber (heartbeat), LED3 = green (breathes).
- "Notification" — short bursts of all three, then long silence.
- "Sunrise" — all three breathing in slightly offset phases.
- "Distress" — synchronous fast strobe on all three.
- Your own theme — name it.
It's done when:
- You can demo all three LEDs producing the named show.
- Switching to it from any other show is instant via the button.
- The show looks visibly different from any other show — no two should be near-identical.
- You can describe in one sentence what mood / scene each show represents.
Recap 5 min
Cluster F closes with the Multi-LED Choreography — three LEDs, three independent patterns, three switchable shows, one button, zero delay. The architectural insight is the stateless pattern function: a function that takes the current millis() reading and decides what the LED should look like right now. Stateless patterns trivially compose into shows, switch instantly between shows, and don't fight each other. Combined with the L02-37 debouncer and the L02-36 tick discipline, you can now build any "many timed things in one loop" system on a single UNO. Cluster G picks up next with EEPROM and SD cards — adding persistence to everything we've built.
- Choreography
- The deliberate orchestration of multiple LEDs (or actuators) over time to produce a coherent visual effect. The same word arts-people use for dance.
- Stateless pattern
- A function that computes the output entirely from the current time, with no internal memory of past calls. Trivially composable and re-entrant.
- Modular arithmetic with millis()
millis() % periodgives the phase within a recurring period. The base trick of every stateless pattern. Wraps cleanly across the 49-day millis() wrap.- Show / scene
- A named combination of which patterns are running on which LEDs. Switching shows = switching the dispatch case.
- Pattern function signature
- Standardising how patterns are called (e.g.
patternBlink(pin, onMs, offMs)) so they can be swapped freely without rewriting the show dispatch. - Triangle wave / sine wave
- Two common brightness shapes for breathing LEDs. Triangle is cheaper to compute (just arithmetic); sine is smoother visually but uses
sin(). - Crossfade
- A smooth transition between two shows by mixing their outputs over a short period. Requires patterns to return brightness rather than write pins directly.
- Cluster F discipline
- The four habits Cluster F instilled: no
delay, one tick per task,millis()with safe-subtract, debounce all buttons. The foundation of every L3 project.
Homework 5 min
Design a four-LED show. Wire a fourth LED on D6 (also PWM). Then design and code one new show that uses all four LEDs distinctly — each LED has its own pattern, ideally with different timings.
Document the show on paper:
- The show's name.
- The intended "mood" or scenario (party, alarm, sunrise, etc.).
- The pattern + timings for each of the 4 LEDs.
- Why those choices match the mood.
Add it to the show enum. Add a 5th "OFF" show for good measure. Cycle through them all with the button.
Bring back next class:
- Your written show design (one page).
- Your
hw-l02-38.inosketch. - A photo or short phone video of the show running.
- Cluster G starts tomorrow — EEPROM, the chip's memory that survives power loss.