Learning Goals 5 min
By the end of this lesson you will be able to:
- Build a single Arduino sketch that does two things at once — runs a continuous LED animation and listens for typed commands — without ever calling
delay()in the animation loop. - Combine every Cluster D idea (begin/println from L01-24, read/available from L01-26, digit conversion from L01-27, switch/case from L01-28) with the
millis()non-blocking pattern previewed in L01-22's stretch task. - Structure a project around three mutable globals —
mode,speed,step— that a tiny command parser updates from the keyboard and an animation engine reads to decide what to display next.
Warm-Up 10 min
Cluster D taught you to make the Arduino talk (L01-24), listen (L01-26), understand digits (L01-27), and dispatch commands cleanly (L01-28). Each of those was a one-trick sketch. Today they all show up in the same project — a remote-controlled LED toy.
Quick-fire puzzle
Look at this sketch — a "blinker" with a delay in it. While it's running, the user types s into the Serial Monitor to stop it. The sketch is reading serial inside loop(). So why does the stop command sometimes take a full second to register?
void loop() {
if (Serial.available() > 0) {
/* … handle a command … */
}
digitalWrite(9, HIGH);
delay(500);
digitalWrite(9, LOW);
delay(500);
}- How often does the
Serial.available()check actually happen? - If you type
sthe instant the LED switches off, when does the sketch notice? - What's the largest possible "I typed but nothing happened" delay before the sketch reacts?
Reveal the answer
- Only once per loop pass — and each pass spends a full second in two
delay(500)calls. So the check runs once a second, not thousands of times a second like a normal loop. - At the very next check — up to a full second later, depending on where in the cycle you typed.
- One second. The user sits and waits, wondering whether their keystroke even reached the Arduino. Awful UX.
The fix isn't "check serial more often" — it's get rid of delay. Today's project does exactly that. The animation timing comes from millis() snapshots, exactly as L01-22's stretch task previewed. The loop runs thousands of times a second, the animation advances on schedule, and every typed command is noticed almost instantly.
New Concept — the command/animation engine pattern 20 min
The big idea — one loop, two jobs
Every interactive Arduino device needs to do two things at once: respond to inputs and keep the outputs moving. With delay, the loop can only do one of them at a time — while it's waiting, nothing else happens. The non-blocking pattern fixes this by replacing "wait for half a second" with "check if half a second has passed".
Today's loop body will have exactly two pieces, in this order:
- Check the keyboard. If a character is waiting, parse it (the L01-28 switch).
- Check the clock. If enough time has passed since the last animation step, advance the animation one step.
Both checks are instant. The loop runs thousands of times a second. Most passes do nothing visible — the buffer is empty and the clock hasn't ticked over. But when an event happens, it's noticed in under a millisecond.
Three mutable globals — the state of the show
The whole project's state lives in three variables at the top of the sketch:
char mode = 's'; // current animation: 's' = stop, 'b' = blink, 'c' = chase, 'a' = alt
int speed = 300; // ms between animation steps
int step = 0; // which frame of the animation we're on
unsigned long lastStep = 0; // millis() snapshot from last advanceThat's the entire model. The command parser updates mode and speed. The animation engine reads mode and uses step to decide which LEDs to light. Each piece is small; together they're a real interactive device.
The non-blocking timing pattern
From L01-22's stretch task, the recipe for "do this every N milliseconds without blocking":
unsigned long now = millis();
if (now - lastStep >= speed) {
lastStep = now;
// do one animation step
}The loop runs constantly, but the body inside the if only fires when speed ms have passed since the last fire. In between, the loop is free to check the Serial buffer thousands of times — which is what lets typed commands feel instant.
Decoding the speed digit — the L01-27 trick
The user types a single digit to set the animation tempo. We use the digit-conversion formula from L01-27 to turn the character into a number, then map it to a sensible delay in milliseconds. A simple mapping: '1' = fast (100 ms), '9' = slow (900 ms), step of 100 ms per digit.
if (isDigit(c) && c != '0') {
speed = (c - '0') * 100; // '3' → 300 ms
}One line of arithmetic — exactly the pattern L01-27 set up. We disallow '0' because a 0 ms step would run as fast as the loop, which is too fast to see.
The animation engine — switch on mode, use step
Each mode is one branch of a switch on mode. The branch reads step (which counts up forever) and decides which LEDs to light. The "blink" mode uses step % 2 to alternate on/off; "chase" uses step % 3 to cycle through the three LEDs; "alt" uses step % 2 with the LEDs split into two groups; "stop" turns everything off and ignores step entirely.
switch (mode) {
case 'b': // blink — all on, all off
if (step % 2 == 0) setLeds(HIGH, HIGH, HIGH);
else setLeds(LOW, LOW, LOW);
break;
case 'c': // chase — one at a time
setLeds(step % 3 == 0, step % 3 == 1, step % 3 == 2);
break;
}The whole engine is six or eight lines. Each frame is one expression. The mode determines which expression runs; step determines which frame within the mode.
Why it matters
This is the smallest version of the loop that lives inside every interactive device you own. A game console's main loop reads the controller while drawing the screen; a microwave's reads the keypad while running the timer; a 3D printer's reads serial commands while stepping the motors. All three follow today's exact shape — check inputs, check the clock, advance the state, repeat — at thousands of cycles per second. Today's six modes are a toy. The pattern is industrial.
Worked Example — build the show 25 min
Goal: a serial-controlled three-LED show with four animation modes (blinked, chase, alternate, stop), an adjustable speed (1–9), and a help menu (?). Same wiring as L01-15 (red, yellow, green on D9, D10, D11, sharing the − rail for GND). No buzzer needed today.
The wiring, and the same circuit on paper
On the breadboard what you build
On paper what engineers draw
The sketch — full listing
// Serial Light Show — Cluster D project
const int RED_PIN = 9;
const int YELLOW_PIN = 10;
const int GREEN_PIN = 11;
char mode = 's';
int speed = 300;
int step = 0;
unsigned long lastStep = 0;
void setLeds(int r, int y, int g) {
digitalWrite(RED_PIN, r);
digitalWrite(YELLOW_PIN, y);
digitalWrite(GREEN_PIN, g);
}
void printHelp() {
Serial.println("--- Serial Light Show ---");
Serial.println("b = blink c = chase a = alternate s = stop");
Serial.println("1-9 = speed (1 fastest, 9 slowest)");
Serial.println("? = this help");
}
void handleCommand(char c) {
switch (c) {
case 'b': case 'B':
case 'c': case 'C':
case 'a': case 'A':
case 's': case 'S':
mode = tolower(c);
step = 0;
Serial.print("mode = ");
Serial.println(mode);
break;
case '1': case '2': case '3':
case '4': case '5': case '6':
case '7': case '8': case '9':
speed = (c - '0') * 100;
Serial.print("speed = ");
Serial.print(speed);
Serial.println(" ms");
break;
case '?':
printHelp();
break;
default:
Serial.print("Unknown: ");
Serial.println(c);
break;
}
}
void animate() {
switch (mode) {
case 'b':
if (step % 2 == 0) setLeds(HIGH, HIGH, HIGH);
else setLeds(LOW, LOW, LOW);
break;
case 'c':
setLeds(step % 3 == 0, step % 3 == 1, step % 3 == 2);
break;
case 'a':
if (step % 2 == 0) setLeds(HIGH, LOW, HIGH);
else setLeds(LOW, HIGH, LOW);
break;
case 's':
default:
setLeds(LOW, LOW, LOW);
break;
}
}
void setup() {
Serial.begin(9600);
pinMode(RED_PIN, OUTPUT);
pinMode(YELLOW_PIN, OUTPUT);
pinMode(GREEN_PIN, OUTPUT);
printHelp();
}
void loop() {
// Job 1: check the keyboard
if (Serial.available() > 0) {
char c = Serial.read();
handleCommand(c);
}
// Job 2: check the clock
unsigned long now = millis();
if (now - lastStep >= (unsigned long)speed) {
lastStep = now;
animate();
step = step + 1;
}
}Walk through what each part does
- The four mutable globals at the top are the entire state of the show. Read them and you can tell exactly what's on screen.
setLedsis the same helper you wrote in L01-15 and L01-23 — drive all three LEDs in one call.printHelpis a separate function so it can be called fromsetup()(the welcome banner) and from the?command. Wrote-once, used twice.handleCommandis the L01-28 switch — letters changemode, digits changespeed,?prints help, anything else falls through to the default complaint. Notice the use oftolower(c)to normalise stacked-case input into a single canonical mode letter.animateis the engine — one branch per mode, each readingstepwith a modulo to pick a frame.loopis just two if-checks: serial-buffer first, clock second. Nothing else. Every other line is in a helper.
Upload, open the Monitor, drive the show
- Upload at 9600 baud. Open the Serial Monitor; set line-ending to "No line ending".
- You should see the help banner appear and the LEDs sit dark (mode =
stop is the default). - Type
b. The LEDs start blinking together at 300 ms. The Monitor confirmsmode = b. - Type
1. The blink speeds up to 100 ms. Type9— it slows to 900 ms. - Type
c. The blink changes to a chase — red, yellow, green, red, yellow, green… - Type
a. The animation switches to alternate: red+green on while yellow off, then yellow on while the others off. - Type
?. The help prints again, without interrupting the animation. - Type
s. Show stops. - Type
x. Monitor saysUnknown: x; animation keeps doing whatever it was doing.
Every command should feel instant — there's no perceptible lag between Send and the show changing. That's the non-blocking pattern doing its job.
Trace one full chase cycle
Suppose mode is 'c' and step just became 0. Fill in the LED states for the next five steps as animate() runs.
| step | step % 3 | red | yellow | green |
|---|---|---|---|---|
| 0 | 0 | HIGH | LOW | LOW |
| 1 | 1 | LOW | HIGH | LOW |
| 2 | ____ | ____ | ____ | ____ |
| 3 | ____ | ____ | ____ | ____ |
| 4 | ____ | ____ | ____ | ____ |
If you can fill in steps 2–4 without uploading, the modulo trick has clicked.
Basic 5 min
Goal: Add a new reverse chase mode triggered by the key r. Instead of red → yellow → green like the regular chase, it goes green → yellow → red.
Plan: add r to the letter group in handleCommand's case stack (so it sets mode = 'r'). Then add a new branch case 'r': in animate() with the LEDs in reverse — use the modulo of 3 but swap which slot lights up. Update printHelp() to mention the new mode.
Questions:
- The forward chase uses
step % 3 == 0for red. What does reverse chase use to light green first? ____ - Why is updating
printHelp()just as important as adding the case? ____ - Could you do this without adding any new case in
handleCommand— just by adding it toanimate()? Why or why not? ____
Challenge 1 5 min
Goal: Add a variable-length blink mode v where the ON time and OFF time are different — red flashes briefly (100 ms on), then yellow sits longer (400 ms on), then green flashes briefly again. The single speed control no longer fits this pattern because each colour wants its own time.
Plan: this is harder than it sounds. The simplest approach: keep a second mutable global int subStep = 0; that counts within a "cycle", and have your v-mode branch use subStep to pick: subStep == 0 → red 100 ms; subStep == 1 → yellow 400 ms; subStep == 2 → green 100 ms. The catch: the time-between-steps now depends on which step you're in.
You'll need a second variable for the "speed of this particular step". One approach:
case 'v':
if (subStep == 0) { setLeds(HIGH, LOW, LOW);
stepSpeed = 100;
}
else if (subStep == 1) { setLeds(LOW, HIGH, LOW);
stepSpeed = 400;
}
else { setLeds(LOW, LOW, HIGH);
stepSpeed = 100;
}
subStep = (subStep + 1) % 3;
break;Then in loop(), use stepSpeed instead of speed for the timer compare only when mode == 'v'.
Questions:
- Why doesn't
speedwork for this mode? ____ - What single global variable lets one mode "lie" to the timer about how long the current step should last? ____
- Most professional animation engines store each step as
{leds, duration}instead of using a chain ofifs. Sketch in your notebook what that would look like for the v mode. ____
Challenge 2 5 min
Goal: Add a pause command that's different from stop. The key p freezes the animation in place (the LEDs stay in whatever state they were in); pressing p again resumes from where it left off; s still resets the show entirely.
Plan: add a fourth mutable global bool paused = false;. The p command flips it. In loop(), only do the animation step if !paused. The timer check should still run — but skip the call to animate() and the step increment when paused. Also make sure to update lastStep when you unpause, otherwise a long pause makes the show "catch up" by firing instantly.
Questions:
- If you forget to update
lastStepon unpause, what happens for the first second or two after resuming, and why? ____ - Why is
pdifferent fromsin terms of which globals it changes? ____ - What change to
?(the help) does this need? ____
Challenge 3 — the bounce mode 10 min
"Make the light ping-pong"
Add a new mode to the Serial Light Show. Typing n starts a bounce. The light goes red → yellow → green → yellow, then repeats. It looks like a ball bouncing between the two ends.
Your task:
- Add
nandNto the stacked group of mode letters inhandleCommand. - Write a step table: for each step, which LED is lit. Count how many steps the pattern takes before it repeats.
- Add a
case 'n':toanimate()that follows your table. Usestep %your count. - Add a line for
ntoprintHelp().
It works if:
- Typing
?shows your new help line. - Typing
nprintsmode = n, and the lights go R, Y, G, Y, R, Y, G, Y … - The speed keys
1–9still change how fast it bounces. sstill stops the show, andb,candastill work.
Recap 5 min
Cluster D's project is a tiny interactive device built around a two-job loop(): check the keyboard, check the clock, do the next thing for whichever fires. Four mutable globals — mode, speed, step, lastStep — hold the entire state. A switch parses single-letter commands; another switch picks the animation. No delay() anywhere in the loop. Type a command, the show changes; sit back, the show runs. This is the shape of every interactive embedded program from here on, and you just built the smallest version of one.
- Non-blocking loop
- A
loop()body that never callsdelay(). It usesmillis()snapshots to decide when enough time has passed, freeing the rest of the loop to check inputs and run other jobs at thousands of cycles per second. - Command parser
- The piece of a sketch that reads incoming characters and decides what each one means. Today's was a switch with stacked cases for case-insensitivity and a default branch for unknown input.
- Animation engine
- The piece of a sketch that drives the outputs over time. Today's was a switch on
modewith one branch per animation, each usingstep(the frame number) to pick what to display. - State variable
- A mutable global that some part of the code writes and another part reads. The parser writes
modeandspeed; the engine reads them. This separation is what lets the two jobs coexist cleanly. - Modulo (
%) for cycling - The expression
step % Ncycles through 0, 1, …, N−1, 0, 1, … forever. The animation engine uses this to pick which frame of an N-frame loop to show right now. - Help command
- The
?convention from old command-line tools: typing it prints a menu of all available commands. Today'sprintHelp()doubles as the startup banner and the runtime help — wrote-once, used-twice.
Extra Mission 5 min
Part 1 — Design your own light-show mode
Your show can blink, chase, alternate and bounce. On paper, design a brand-new mode of your own. Ideas: a heartbeat, a Morse SOS on red, or a random colour every step using random(3).
Your design must include:
- A letter key that is not already used (
b c a s n ?and1–9are taken). - A step table: for step 0, 1, 2 and so on, which of the three LEDs are on.
- How many steps the pattern takes before it repeats — the number after the
%. - Whether your mode follows the speed keys or ignores them.
- The new line for
printHelp().
Part 2 — Make it
Add your mode to the Serial Light Show. Add the letter to the stacked group in handleCommand. Add a new case to animate(). Add your line to printHelp().
Bring back next class:
- The saved
.inofile. - A Serial Monitor screenshot showing your help line and
mode =your letter. - A short video of your mode running.
Heads up for next class: Cluster D is done. L01-30 "RGB LED Wiring" opens Cluster E — a single tiny package that holds three LEDs (red, green, blue) inside one dome. You'll learn the two ways its pins can be arranged (common-cathode vs common-anode) and wire one onto your breadboard ready for the colour-mixing lesson that follows.