Traffic Light with all four of the
book's "try this next" exercises done. A real WALK state instead of just
cutting green short, a pedestrian LED blinking on its own 500ms rhythm while the
main machine keeps running, r g y w typed into the Serial Monitor to
force a state, and a press counter to make bounce countable.
Cycle is RED 4s, GREEN 6s (or 1.5s if someone is waiting), YELLOW 1.5s, WALK 4s, then back to GREEN. During WALK the car lights hold red and the walk LED on D5 blinks four times.
Adding a state moved the flag
P04 taught that you clear a flag where the request is satisfied. What it could not show is that "where" is not fixed.
Adding WALK between YELLOW and GREEN meant walkRequested had to move its clear
from the GREEN to YELLOW transition down to WALK to GREEN, because WALK is now
the thing that actually satisfies the request. A flag is a contract between
whoever sets it and whoever clears it, and inserting a state silently rewrites
that contract. Nothing in the compiler catches it. The old code still compiles
and still runs, just wrong.
Also worth knowing: RED and WALK both light the red LED, so they look identical on the breadboard. They are different states because their transitions differ, not their outputs. If the difference is only a number, use data. If it is behaviour, use a state.
The log line that lied
YELLOW has two exits but started with one print, sitting after the if/else
where both paths converge. So going into WALK announced itself as -> RED. The
lights were doing the right thing and the log was lying about it, and because the
LEDs go red during WALK anyway, the display backed up the wrong story.
Fix was moving each print inside its own branch, next to the assignment it
describes. stateStart = millis() does belong after the merge, because it is
genuinely common to both paths. Telling those two apart is the whole lesson.
What I learned
- A log line belongs with the branch that caused it. Hoist it to where paths converge and it drifts out of sync with the code. A print that survives a refactor untouched is the one most likely to be wrong
digitalWriteon a pin still set toINPUTtoggles the internal pull-up instead of driving the pin. About 0.1 mA, an invisible glow that looks exactly like dead hardware. CheckpinModebefore blaming the breadboard- Timed behaviour that lives and dies with one state can come straight off
elapsed.blinkOn = ((elapsed / HALF) % 2 == 0)needs no extra globals and cannot drift out of sync. Only use a separate timer when the behaviour outlives the state - An override only jumps. Set the state, re-stamp
stateStart, print. Theswitchalready knows how to be each state, so do not duplicate its body. Forget the re-stamp and the new state inherits an old clock and exits instantly Serial.available()inspects,Serial.read()consumes. Check then consume, becauseread()on an empty buffer returns -1- The Serial Monitor is just an app at the far end of a pipe. Interchangeable with PuTTY or a Python script. Baud has to match on both sides because UART has no clock line
- The sketch runs from flash, not off the wire. After upload, PC to Arduino carries nothing until I type. Unplug USB and the board runs the same
- Inbound path is
UDR0(one byte, in hardware), then an interrupt, then the RX ring buffer (64 bytes, in RAM), thenSerial.read(). The interrupt comes free withSerial.begin.read()is the only part I write UDR0moves nothing. Hardware raises flags, code moves bytes. Same division of labour everywhere on this chip, including the timer behindmillis()Serial.printis not sending anything. It drops bytes in a queue and walks away. Transmission happens later on the interrupt's schedule at wire speed- TX exists because the wire is slow, roughly 1.04 ms per character at 9600 baud,
about 16,000 instructions' worth. RX exists because
UDR0holds one byte, so the buffer turns a 1 ms hardware deadline into a soft 64 ms one - The asymmetry that matters: on TX I am the producer so I can be made to wait,
and
Serial.printblocking means data is safe but timing is damaged. On RX the outside world is the producer and cannot be made to wait, so bytes are dropped and data is lost while timing is fine. First question at any producer and consumer boundary is which side am I - Bounce is mechanical and no code prevents it. At about 50 µs sampling I get roughly 40 looks across the bounce, so one press fires several edges. Release bounces too
- P04 hid bounce because setting an already-true flag is idempotent. The counter does not fix anything, it makes an invisible event countable so the debounce dead time comes from measuring my own hardware. The moment an edge does something non-idempotent, three edges means three actions