Learning Goals 5 min
By the end of this lesson you will be able to:
- Use the term finite state machine (FSM) correctly — a fixed set of named states, well-defined transitions between them, and exactly one "current state" at any moment — and recognise that you've been building FSMs since L01-23.
- Wire and code a four-state traffic-light controller with car LEDs (R/A/G), a pedestrian LED, a pedestrian-request button, and a minimum-green guard so the button can't be spammed to lock cars out.
- Draw a clean state diagram on paper (states as labelled circles, transitions as arrows with triggers) and use it as the single source of truth that both the wiring and the code follow.
Warm-Up 10 min
Cluster G's two earlier Builds turned a sensor into a game (L01-44) and a string into a Morse signal (L01-45). Today is a closing-the-loop kind of build: the most universally recognised state machine in the world — the road junction with traffic lights and a pedestrian crossing. Anyone can spot the states and transitions because everyone's seen one.
Quick-fire puzzle
Stand next to a busy pedestrian crossing for two minutes (mentally — you don't have to leave the room). Watch the cycle. Now answer:
- How many distinct states does the system have? Name them in plain English — focus on what the lights are doing, not how long each phase lasts.
- What causes the system to move from one state to the next — a person, a timer, both?
- If you press the pedestrian button right after the cars have just turned green, does the system snap straight to "stop cars now"? Why not?
Reveal the answer
- Four obvious states: cars green (peds wait), cars amber (peds wait, cars slowing), peds walking (cars stopped), peds flashing (warning ends soon, cars still stopped). UK also has a red+amber state where cars prepare to go — we'll handle that in an extension.
- Almost all transitions are timer-driven: each state has a duration, and at the end of it the system moves to the next. The pedestrian button is the only external input — it doesn't immediately switch state; it asks for a pedestrian phase the next time it's safe.
- It doesn't snap because of the minimum-green guard: cars are given a guaranteed minimum on-time (e.g. 5 seconds) before any button can shorten the cycle. Without this, one rude pedestrian could push every car waiting at the junction to a stop. Real intersections all have one — it's a fairness rule, not a bug.
This is a finite state machine in the wild. Four states. Mostly timer-driven transitions. One external input that requests but doesn't immediately cause a transition. Bounded fairness. Today you'll build all of it.
New Concept — what an FSM really is 15 min
The big idea — a name for a pattern you already use
You've been building finite state machines (FSMs) since L01-23. The burglar alarm had 2 states; the light show had 4; the reaction game had 5. Today: 4 again, but with the formal name attached.
A finite state machine has four ingredients, no more, no less:
- A finite set of named states. Today:
CAR_GREEN,CAR_AMBER,PED_WALK,PED_FLASH. - A set of transitions — explicit "from state X, on event Y, go to state Z" rules. No transitions = state is final.
- A current state. Exactly one at any moment. Stored in a global variable like
int state. - A set of inputs/events — what the FSM listens to (a timer expiring, a button press, a sensor crossing a threshold).
That's it. Every FSM in this syllabus (and every FSM in industry) is some configuration of those four ingredients.
The four states for our traffic light
| State | Car red | Car amber | Car green | Ped LED | Duration (default) |
|---|---|---|---|---|---|
CAR_GREEN | off | off | on | off (don't walk) | 10 s (or until button + min) |
CAR_AMBER | off | on | off | off (don't walk) | 3 s |
PED_WALK | on | off | off | on (walk) | 6 s |
PED_FLASH | on | off | off | flashing | 4 s |
Total cycle: 23 seconds. Each state has a clear LED pattern (one row of the table) and a clear duration. The pedestrian button can shorten CAR_GREEN's duration down to (but not below) a 5-second minimum.
The state diagram — draw it before you code it
CAR_GREEN's timer (subject to the minimum-green guard). Draw this kind of diagram before writing the code; the diagram becomes the specification the code has to match.The button-request rule — fairness, not immediacy
The pedestrian button does not immediately stop the cars. Instead it sets a flag — "someone wants to cross" — that the FSM reads when deciding how long to stay in CAR_GREEN:
- No request pending: stay in
CAR_GREENfor the full default (10 s). - Request pending and at least 5 s of green has already elapsed: transition immediately to
CAR_AMBER. - Request pending but less than 5 s has elapsed: keep waiting until 5 s, then transition.
This is the minimum-green guard. It's what real junctions use to prevent bandits from stopping traffic on demand. The number (5 s) is a fairness parameter you'd tune for the road type — busier roads get longer minimums.
The non-blocking pattern, again
Like L01-29 and L01-43, the loop is non-blocking: it runs thousands of times a second, checks the clock, checks the button, and only does work at the moments when something needs to change. The whole top-level structure is:
void loop() {
// 1. Update the request flag from the button (state-change detection).
// 2. Compute how long we've been in the current state.
// 3. Decide if it's time to transition based on duration + request.
// 4. If transitioning: change state, set its LEDs, record the new entry time.
// 5. If staying: maybe blink the ped LED (PED_FLASH does that on its own timer).
}The clever bit is step 5 — PED_FLASH needs to blink at 4 Hz while staying in the same state. That's done by reading millis() and toggling the LED based on which "half-second slot" we're in. Same trick as L01-22's non-blocking blink.
Why it matters
The traffic-light pattern is everywhere — not just at intersections. Microwaves cook through cook/done/idle states. ATMs walk you through idle/login/menu/transaction/print/exit. Industrial robots step through wait/load/process/unload. Door locks. Elevators. Game UI. If a device responds to time and inputs, it's a finite state machine. Today is the last time we'll formalise it in this syllabus; from Level 2 onwards, you should reach for FSMs by reflex.
Worked Example — wire it, draw it, code it 25 min
The wiring
Three car LEDs + one pedestrian LED + one button — all on a single board sharing GND:
- Car red on D11, car amber on D10, car green on D9, each with a 220 Ω resistor (L01-15 layout — same as the original Cluster B traffic light).
- Pedestrian LED on D6 with a 220 Ω resistor.
- Pedestrian button on D7 with
INPUT_PULLUP(L01-17 layout). - One − rail GND bus, one wire back to the Arduino's GND pin.
The full sketch
// Traffic-light controller + pedestrian crossing — 4-state FSM
const int CAR_RED = 11;
const int CAR_AMB = 10;
const int CAR_GRN = 9;
const int PED_LED = 6;
const int PED_BTN = 7;
// State labels
const int CAR_GREEN = 0;
const int CAR_AMBER = 1;
const int PED_WALK = 2;
const int PED_FLASH = 3;
// Durations (ms)
const unsigned long CAR_GREEN_MS = 10000;
const unsigned long CAR_AMBER_MS = 3000;
const unsigned long PED_WALK_MS = 6000;
const unsigned long PED_FLASH_MS = 4000;
const unsigned long MIN_GREEN_MS = 5000;
const unsigned long FLASH_PERIOD = 250;
// State variables
int state = CAR_GREEN;
unsigned long stateEntryTime = 0;
bool requestPending = false;
int lastButton = HIGH;
// === Apply a state's LED pattern (called once on transition) ===
void applyState(int s) {
digitalWrite(CAR_RED, s == PED_WALK || s == PED_FLASH);
digitalWrite(CAR_AMB, s == CAR_AMBER);
digitalWrite(CAR_GRN, s == CAR_GREEN);
digitalWrite(PED_LED, s == PED_WALK); // flash handled separately
}
void enterState(int next) {
state = next;
stateEntryTime = millis();
applyState(next);
Serial.print("→ ");
if (next == CAR_GREEN) Serial.println("CAR_GREEN");
else if (next == CAR_AMBER) Serial.println("CAR_AMBER");
else if (next == PED_WALK) Serial.println("PED_WALK");
else Serial.println("PED_FLASH");
}
bool buttonPressed() {
int now = digitalRead(PED_BTN);
bool edge = (lastButton == HIGH && now == LOW);
lastButton = now;
return edge;
}
void setup() {
Serial.begin(9600);
pinMode(CAR_RED, OUTPUT);
pinMode(CAR_AMB, OUTPUT);
pinMode(CAR_GRN, OUTPUT);
pinMode(PED_LED, OUTPUT);
pinMode(PED_BTN, INPUT_PULLUP);
enterState(CAR_GREEN);
}
void loop() {
// 1. Latch any button press into the request flag
if (buttonPressed()) {
requestPending = true;
Serial.println("(request received)");
}
// 2. How long have we been in the current state?
unsigned long elapsed = millis() - stateEntryTime;
// 3. State-specific transition logic
switch (state) {
case CAR_GREEN: {
bool defaultDone = (elapsed >= CAR_GREEN_MS);
bool shortcutOK = (requestPending && elapsed >= MIN_GREEN_MS);
if (defaultDone || shortcutOK) {
enterState(CAR_AMBER);
}
break;
}
case CAR_AMBER:
if (elapsed >= CAR_AMBER_MS) {
requestPending = false; // request being honoured now
enterState(PED_WALK);
}
break;
case PED_WALK:
if (elapsed >= PED_WALK_MS) enterState(PED_FLASH);
break;
case PED_FLASH:
// While in PED_FLASH: blink the ped LED at 2 Hz
digitalWrite(PED_LED, (elapsed / FLASH_PERIOD) % 2);
if (elapsed >= PED_FLASH_MS) {
digitalWrite(PED_LED, LOW);
enterState(CAR_GREEN);
}
break;
}
}Walk through what each part does
- Pin and state constants at the top — every magic number named. Change durations in one place.
applyState(s)— sets all four LEDs based on which state we're entering. The fourdigitalWritecalls with boolean expressions (s == PED_WALK || s == PED_FLASH) replace four separateifbranches. Pure L01-42 boolean logic.enterState(next)— the "transition" helper. Updatesstate, resets the timer, applies LEDs, prints the new state name. Called from every transition inloop().buttonPressed()— state-change detection from L01-19. Returns true exactly once per physical press.- The switch in
loop()— one case per state, each handling its own transition logic.CAR_GREENis the only state with two competing transition rules (default timer vs button shortcut); the rest just wait for their timer. - The flashing trick in
PED_FLASH:(elapsed / FLASH_PERIOD) % 2evaluates to 0/1/0/1/… as time passes, toggling every 250 ms. No state variable needed for the blink — the elapsed time itself encodes which "slot" we're in.
Upload and watch one full cycle
- Upload at 9600 baud. Open the Monitor. Banner:
→ CAR_GREEN. Car green LED on, the rest off. - Wait. After 10 seconds the Monitor prints
→ CAR_AMBER, the green goes off, amber comes on. - After 3 more seconds:
→ PED_WALK. Red on, amber off, pedestrian LED steady on. - 6 seconds:
→ PED_FLASH. Pedestrian LED starts blinking 2× per second. - 4 seconds:
→ CAR_GREEN. Cycle restarts. - Now press the pedestrian button right at the start of
CAR_GREEN. Monitor:(request received). After exactly 5 seconds (the minimum-green guard) the Monitor prints→ CAR_AMBER. Without your button press, you'd have waited 10. - Press the button 6 seconds into
CAR_GREEN(after the minimum has already passed). The transition fires almost immediately — the FSM honours the request the moment it sees it. - Press the button repeatedly during
PED_WALK. Monitor prints "request received" every time, but nothing changes — pedestrians are already crossing. The flag is set and reset cleanly each cycle.
Trace one full cycle on paper
Assume you press the button 2 s after entering CAR_GREEN. Fill in the time at which each state transition fires.
| Event | Time (s) | State after |
|---|---|---|
| Boot — enter CAR_GREEN | 0 | CAR_GREEN |
| Button press — request latched | 2 | CAR_GREEN (still) |
| Shortcut fires (because elapsed ≥ 5 AND requestPending) | ____ | ____ |
| CAR_AMBER timer up | ____ | ____ (and requestPending → false) |
| PED_WALK timer up | ____ | ____ |
| PED_FLASH timer up | ____ | ____ |
The whole cycle ends up taking 5 + 3 + 6 + 4 = 18 seconds when shortened by a press (vs 23 s without). That's the minimum-green guard's gift — fast for pedestrians, fair to cars.
Try It Yourself — three FSM polish moves 15 min
Goal: Add an audio cue. When the FSM transitions from CAR_AMBER to PED_WALK, beep the buzzer once at 1000 Hz for 100 ms — like real pedestrian crossings do.
Plan: add a piezo on D8. Pop a tone(8, 1000, 100); at the top of the CAR_AMBER → PED_WALK transition. No new state needed.
case CAR_AMBER:
if (elapsed >= CAR_AMBER_MS) {
requestPending = false;
tone(8, 1000, 100); // "walk now" beep
enterState(PED_WALK);
}
break;Questions:
- Where else in the FSM might you want an audio cue? ____
- Real crossings use repeated beeps during the WALK phase to help vision-impaired users. How would you add that without blocking the loop? ____ (Hint: same
(elapsed / period) % 2trick as PED_FLASH.) - Why is the audio cue placed inside the transition rather than as a one-off in
applyState(PED_WALK)? ____ (Hint: applyState is also called on boot — you don't want a beep at power-on.)
Goal: Add the UK CAR_RED_AMBER state. After PED_FLASH, instead of going straight to CAR_GREEN, light both red and amber for 2 seconds — signalling "cars get ready to go" — then transition to green. This is what every UK traffic light actually does.
Plan: add a fifth state. Add it to the state-label constants, give it a duration, and slot it into the cycle between PED_FLASH and CAR_GREEN.
const int CAR_RED_AMBER = 4;
const unsigned long CAR_RED_AMBER_MS = 2000;
// In applyState, both car red and amber on for this state:
digitalWrite(CAR_RED, s == PED_WALK || s == PED_FLASH || s == CAR_RED_AMBER);
digitalWrite(CAR_AMB, s == CAR_AMBER || s == CAR_RED_AMBER);
// Modify PED_FLASH's exit:
case PED_FLASH:
digitalWrite(PED_LED, (elapsed / FLASH_PERIOD) % 2);
if (elapsed >= PED_FLASH_MS) {
digitalWrite(PED_LED, LOW);
enterState(CAR_RED_AMBER); // not CAR_GREEN!
}
break;
// Add a new case:
case CAR_RED_AMBER:
if (elapsed >= CAR_RED_AMBER_MS) enterState(CAR_GREEN);
break;Questions:
- Update the state diagram in your notebook — how many states now, how many transitions? ____
- Why is there no equivalent "amber-only" preparation state between CAR_GREEN and PED_WALK — i.e. why don't UK lights go GREEN → RED_AMBER → RED? ____ (Hint: "get ready to go" makes sense; "get ready to stop" doesn't need a special light because stopping is instant.)
- What's the total cycle time now, with and without a button shortcut? ____
Goal: Add a "pedestrian wait" indicator. Add another LED on D5. While a request is pending but pedestrians haven't yet been served, this LED glows steady to confirm to the pedestrian that their press was registered. Goes off the moment PED_WALK begins.
This is exactly what the orange "WAIT" word does on a real UK Pelican crossing.
Plan: drive the new LED in loop() right after the button latch — set it on iff requestPending is true and state is not yet PED_WALK / PED_FLASH.
const int WAIT_LED = 5;
// In setup():
pinMode(WAIT_LED, OUTPUT);
// In loop(), after latching the button press:
bool waitingForPed = requestPending && (state == CAR_GREEN || state == CAR_AMBER);
digitalWrite(WAIT_LED, waitingForPed);Questions:
- Why is the WAIT condition checked on every loop pass, rather than only inside transition handlers? ____ (Hint: it depends on the live state AND the live request flag.)
- The WAIT LED stays on through CAR_AMBER as well. Is that right? Why or why not? ____ (Hint: the pedestrian still hasn't been served — it stays on until they actually start crossing.)
- What's the truth table for "WAIT LED on" with two inputs (requestPending, state)? List all eight combinations. ____
Mini-Challenge — emergency override 10 min
"Hold the button for 3 seconds → cars stop, peds walk, NOW"
Real intersections have an emergency mode (used by fire trucks, ambulances) that overrides all the normal logic and forces cars to red. Today's version: hold the pedestrian button down continuously for 3 seconds, and the FSM jumps straight to PED_WALK without waiting for any timer.
Your task:
- Track when the button first goes LOW with
unsigned long buttonDownSince. - If the button is currently held LOW and
millis() - buttonDownSince >= 3000, and we're not already in PED_WALK or PED_FLASH, transition immediately to PED_WALK. - Print
"EMERGENCY OVERRIDE"to the Monitor when this fires. - The normal "press to request" behaviour should still work for short presses — emergency only triggers for a sustained 3-second hold.
It works if:
- A short tap (under 1 s) acts like a normal request — eventually transitions after the minimum-green time.
- A 3-second hold during CAR_GREEN immediately transitions to PED_WALK (the FSM skips CAR_AMBER entirely — that's the override).
- A 3-second hold during PED_WALK or PED_FLASH does nothing extra (you're already serving pedestrians).
- Releasing the button at any time resets the hold timer.
Reveal the additions
unsigned long buttonDownSince = 0;
// In loop(), after the existing button-press latch:
int now = digitalRead(PED_BTN);
if (now == LOW) {
if (buttonDownSince == 0) buttonDownSince = millis();
if (millis() - buttonDownSince >= 3000
&& state != PED_WALK && state != PED_FLASH) {
Serial.println("EMERGENCY OVERRIDE");
requestPending = false;
enterState(PED_WALK);
buttonDownSince = 0;
}
}
else {
buttonDownSince = 0; // released; reset hold timer
}Three rules in five lines. buttonDownSince = 0 means "not currently held"; any non-zero value is the timestamp of the press start. The override fires once when the 3 s mark is crossed (and we're not already serving peds), then resets buttonDownSince so it doesn't keep firing. Releasing the button always resets the timer. This is a real FSM-with-real-product polish move — a "long press" feature on top of a "short press" feature, both using the same physical input.
Recap 5 min
Finite state machines have four parts: states, transitions, a current state, and inputs. You've been building them since L01-23 — today is the lesson where the name and the formal structure get attached. The traffic-light controller is the cleanest possible example: four states arranged in a clockwise loop, three timer-driven transitions, one button-triggered shortcut bounded by a minimum-green fairness guard. The whole pattern fits in 70 lines of code and maps 1-to-1 onto a real-world device you walk past every day. From Level 2 onwards, you'll reach for FSMs by reflex — for any sketch where "what to do next" depends on "what just happened" plus "how long ago".
- Finite state machine (FSM)
- A formal model with four parts: a finite set of named states, transitions between them, exactly one current state at any moment, and a set of inputs/events that drive transitions. The universal pattern for time-and-input-driven behaviour. Industry shorthand for "the right way to structure interactive code".
- State
- A named mode the system is in. Each state has its own behaviour (what to output, what timer to track) and its own transition rules (when to leave).
- Transition
- The act of changing from one state to another. Typically triggered by a timer expiring or an external event (button press, sensor reading crossing a threshold). In code:
enterState(NEXT). - State entry / exit action
- Code that runs once at the moment of transition — like setting LEDs or playing a tone. Distinct from "while-in-state" behaviour that runs every loop pass.
- Timer-driven transition
- A transition that fires when the time spent in the current state reaches a threshold. Implemented by recording
stateEntryTime = millis()on entry and checkingmillis() - stateEntryTime >= duration. - State diagram
- A visual representation of an FSM: each state as a labelled circle, each transition as a labelled arrow. The single source of truth for the design — draw it before coding, refer back as the spec.
- Minimum-green guard (fairness constraint)
- A rule that gives a state a guaranteed minimum duration before any external input can shorten it. Used in traffic lights to prevent pedestrians from instantly stopping cars; used in product UIs to prevent button-mashing from skipping required steps.
- Latched request
- A flag that gets set by an event (button press) and stays true until handled (pedestrian phase begins). Lets the FSM "remember" requests across time, not just react to them at the millisecond of arrival.
Homework 5 min
The traffic FSM, drawn from memory. No Arduino needed for the main task. On a single notebook page, draw the state diagram for today's traffic-light controller. Include:
- All five states (the worked example's four plus the UK CAR_RED_AMBER if you did the medium task). Each as a labelled circle with its default duration.
- Every transition as an arrow with its trigger ("timer", "button request", "button hold 3s").
- The button request flow, including the latched
requestPendingflag. - If you did the stretch task, the WAIT-LED indicator's "on" condition as a small truth table off to the side.
Then upload a sketch and verify each transition you drew actually happens in the same order. (No new code required — just check your diagram matches reality.)
Also: a design reflection on paper.
- Look back at L01-23 (burglar alarm, 2 states), L01-29 (light show, 4 states), L01-44 (game, 5 states), and today's (4 or 5). Across all four, which had the most transitions? Which had the most inputs? ____
- FSMs scale to thousands of states (real industrial controllers have hundreds). At what state count would you stop drawing the diagram by hand? What tool would you use instead? ____ (Hint: dedicated software — Stateflow, XState, etc.)
- Today's minimum-green guard is 5 seconds. What would happen on a busy main road with a 5-second guard vs a 30-second guard? What's the trade-off? ____
- Real intersections have a concurrent traffic-light controller — when one direction is green, the cross direction is red, locked in by the same FSM. How would you extend today's code for a four-way intersection? Sketch the new states. ____
Bring back next class:
- Your hand-drawn state diagram on a notebook page (photo or scan if you want to keep the original).
- A 30-second phone video showing one full cycle, with at least one button press to demonstrate the shortcut.
- Your four written reflection answers, in your notebook.
Heads up for next class: L01-47 "Planning on Paper" isn't a coding lesson at all — it's a deliberate step back. Before Level 1 ends, you'll learn the workflow professional engineers actually use: sketch the wiring on paper, draw the state diagram, list the components, plan the data structures — then open the IDE. The whole lesson is about closing the gap between "I had an idea" and "I have a working sketch".