Learning Goals 5 min
- Build a tiny reaction-timer game (a quick re-spin of L01-22) where the "best ever" reaction time is saved to EEPROM and persists across power cycles.
- Apply
EEPROM.put / getfor anunsigned long(a 4-byte value) — the high score in milliseconds. - Add a long-press "reset high score" gesture and reuse the L02-37 debounced button to detect it.
Warm-Up 10 min
The high-score is the canonical EEPROM application. Every old arcade machine, every Game Boy cartridge, every gym fitness tracker has the same shape: a small value (current best time, distance, score) saved persistently, displayed prominently, occasionally beaten and overwritten. Today we build the simplest possible version of this for a reaction-timer game.
New Concept · The high-score pattern 20 min
The four-step pattern
- On boot: read the saved high score from EEPROM into a RAM variable.
- During play: the RAM variable is the current best. Display it on screen / Serial.
- After each game: if this game's result beat the current best, update both RAM and EEPROM. Otherwise do nothing — no EEPROM write needed.
- Provide a reset: some gesture (long press, special button combo) wipes the high score back to a sentinel.
The sentinel for "no score yet"
For a reaction time, a real score is going to be 150–800 ms typically. 0xFFFFFFFF (max unsigned long, ~4 billion) is an "impossibly slow" reaction time and serves as a perfect sentinel. On a fresh EEPROM, every byte is 0xFF, so an uninitialised unsigned long at any address reads back as exactly 0xFFFFFFFF — meaning "no score yet" with no extra magic number needed.
const unsigned long NO_SCORE = 0xFFFFFFFF;
unsigned long high;
EEPROM.get(0, high);
if (high == NO_SCORE) {
Serial.println("No high score yet.");
} else {
Serial.print("High score: ");
Serial.print(high);
Serial.println(" ms");
}Updating the high score
unsigned long thisGame = endTime - startTime;
if (thisGame < high) { // lower time = better (it's a reaction timer)
high = thisGame;
EEPROM.put(0, high);
Serial.println("NEW HIGH SCORE!");
} else {
Serial.print("Result: ");
Serial.print(thisGame);
Serial.print(" ms (best: ");
Serial.print(high);
Serial.println(" ms)");
}For a reaction timer the comparison is < (lower = better). For most other games it's > (higher = better). Don't mix them up.
Reset gesture
From the L02-37 challenge — the long-press flag. Hold the button for 2 seconds:
if (btn.longPressFired) {
high = NO_SCORE;
EEPROM.put(0, high);
Serial.println("High score reset.");
}Worked Example · Reaction timer with high score 25 min
Step 1 — wiring
| Component | Pin |
|---|---|
| LED (with 220 Ω) | D9 |
| Button (INPUT_PULLUP, to GND) | D2 |
| Buzzer (optional, for go-tone) | D8 |
Step 2 — the sketch
Save as reaction-high-score.ino:
// L02-40: Reaction timer with EEPROM-persistent high score
#include <EEPROM.h>
const int LED = 9;
const int BTN = 2;
const int BUZZ = 8;
const int HIGH_ADDR = 0;
const unsigned long NO_SCORE = 0xFFFFFFFF;
unsigned long highScore = NO_SCORE;
enum State { WAITING, READY, ARMED, FALSE_START };
State state = WAITING;
unsigned long stateEnteredAt = 0;
unsigned long armDelay = 0;
unsigned long armedAt = 0;
// ---- Debounced button (from L02-37) ----
struct Button {
int pin;
unsigned long lastChange;
int rawState, stableState, prevStable;
bool justPressed, longPressFired;
unsigned long pressedAt;
void begin(int p) {
pin = p;
pinMode(pin, INPUT_PULLUP);
rawState = stableState = prevStable = digitalRead(pin);
lastChange = millis();
justPressed = false;
longPressFired = 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;
pressedAt = millis();
longPressFired = false;
}
}
if (stableState == LOW && !longPressFired && millis() - pressedAt >= 2000) {
longPressFired = true;
}
if (stableState == HIGH) longPressFired = false;
}
};
Button btn;
void announceHigh() {
Serial.print("HIGH: ");
if (highScore == NO_SCORE) Serial.println("(none yet)");
else { Serial.print(highScore);
Serial.println(" ms");
}
}
void enterState(State next) {
state = next;
stateEnteredAt = millis();
switch (next) {
case WAITING:
digitalWrite(LED, LOW);
Serial.println("Press the button to start.");
announceHigh();
break;
case READY:
digitalWrite(LED, LOW);
armDelay = random(2000, 5000); // 2-5 s suspense
Serial.println("Ready... wait for GREEN.");
break;
case ARMED:
digitalWrite(LED, HIGH);
armedAt = millis();
tone(BUZZ, 1500, 60);
Serial.println("GO!");
break;
case FALSE_START:
digitalWrite(LED, LOW);
tone(BUZZ, 300, 300);
Serial.println("FALSE START! Press to retry.");
break;
}
}
void setup() {
pinMode(LED, OUTPUT);
pinMode(BUZZ, OUTPUT);
Serial.begin(9600);
randomSeed(analogRead(A0));
btn.begin(BTN);
EEPROM.get(HIGH_ADDR, highScore);
enterState(WAITING);
}
void loop() {
btn.tick();
// long-press reset (works in any state)
if (btn.longPressFired) {
highScore = NO_SCORE;
EEPROM.put(HIGH_ADDR, highScore);
Serial.println(">> high score reset");
btn.longPressFired = false; // consume it
enterState(WAITING);
return;
}
switch (state) {
case WAITING:
if (btn.justPressed) enterState(READY);
break;
case READY:
// If the user presses before ARMED → false start
if (btn.justPressed) enterState(FALSE_START);
else if (millis() - stateEnteredAt >= armDelay) enterState(ARMED);
break;
case ARMED:
if (btn.justPressed) {
unsigned long reaction = millis() - armedAt;
Serial.print("Your time: ");
Serial.print(reaction);
Serial.println(" ms");
if (reaction < highScore) {
highScore = reaction;
EEPROM.put(HIGH_ADDR, highScore);
Serial.println("** NEW HIGH SCORE **");
}
enterState(WAITING);
}
break;
case FALSE_START:
if (btn.justPressed) enterState(WAITING);
break;
}
}Step 3 — upload and play
Open Serial Monitor. First boot:
Press the button to start. HIGH: (none yet)
Press the button. Wait. LED turns on, buzzer chirps:
Ready... wait for GREEN. GO! Your time: 412 ms ** NEW HIGH SCORE ** Press the button to start. HIGH: 412 ms
Step 4 — beat your own record
Play again. If you go faster, "** NEW HIGH SCORE **" fires and the new time is saved. If you go slower, the score line shows your result and the high score stays put.
Step 5 — power-cycle test
Unplug the USB. Re-plug. Watch the Serial Monitor. The first line should now show HIGH: 412 ms (or whatever you achieved). The score persisted across power loss. That's EEPROM in action.
Step 6 — try the reset gesture
Hold the button down for 2 full seconds. You should see:
>> high score reset Press the button to start. HIGH: (none yet)
Fresh slate. The long-press from L02-37 makes for a clean "factory reset" without needing a second button.
Step 7 — false-start handling
Press the button before the LED turns on. The sketch detects this in the READY state and goes to FALSE_START. The score doesn't get updated (because we don't even compute one). Press again to retry. Same logic that real reaction-tester arcade games use — you can't cheat by mashing the button before the signal.
Basic 5 min
Goal: Also save the worst time (slowest reaction since you got the game). Print both on the WAITING screen. EEPROM now stores 8 bytes total — high score (best) at address 0, worst at address 4.
Challenge 1 5 min
Goal: Save the top THREE scores, not just the top one. Each new score should be inserted into the right place if it beats any of them.
Challenge 2 5 min
Goal: Put it all on the LCD. Use the L02-29 / L02-31 patterns to show the high score, the current state, and the last result on a 16×2 screen — no Serial needed.
Challenge 3 · A different game, same persistence 15 min
Pick any other simple game or measurement and apply the high-score pattern to it. Some ideas:
- "Hold steady": press and hold the button as close to exactly 1 second as you can. Score = how close you got (lower is better). Save your best.
- "Count to ten": press the button as fast as you can ten times. Save your best total time.
- "Distance precision": using the HC-SR04, hold your hand as close to exactly 25 cm as you can for 3 seconds. Save the smallest average error.
- "Resistance to chaos": shake the Arduino with a tilt switch; save the longest streak without any tilt detected.
It's done when:
- The game has a clear start, end, and scoring rule.
- The high score persists across power cycles.
- There's a reset gesture (long press, double-press, special combo — your call).
- The current high score is shown prominently on every "ready" state.
Recap 5 min
The high-score pattern is the simplest interesting use of EEPROM: read on boot, update only when the new value beats the old, save with EEPROM.put for any non-byte type. Using 0xFFFFFFFF (the value an uninitialised EEPROM naturally holds) as the "no score yet" sentinel saves a magic-number byte and works on a fresh chip with no special handling. The long-press reset (from L02-37) makes for a clean "factory wipe" without burning an extra physical control. Tomorrow we step up to SD cards — the next level of persistent storage, for projects whose data won't fit in 1 KB.
- High score pattern
- Read-on-boot, compare-on-event, save-if-better. The canonical structure for persistent "best ever" values.
- Sentinel value
- An out-of-range value used to mean "no data". For unsigned longs, 0xFFFFFFFF works perfectly because that's what fresh EEPROM holds.
EEPROM.put / getwith multi-byte types- The templated form of EEPROM access. Handles
int(2 bytes),unsigned long(4),float(4), arrays, even structs. Address you give is the starting byte. - Read-modify-write
- The general pattern of reading a value from persistent storage, modifying it in RAM, then writing it back. Used for any "update one piece of saved data" operation.
- State machine for a game
- WAITING → READY → ARMED → (back to WAITING) plus a FALSE_START escape. The state machine pattern from Cluster D applied to a tiny game loop.
- Long-press as a UI gesture
- Holding the button for 2+ seconds = secondary action. Lets one button do double duty (here: short = start game, long = reset high score).
- Random with seed
random(min, max)returns a pseudo-random integer in [min, max). Seed it once withrandomSeed(analogRead(A0))so the random sequence differs between runs.- Bragging rights
- The actual UX value of a high score. Display it prominently. The whole reason for the persistence.
Extra Mission 5 min
Part 1 — Design a personal-best tracker
Pick an everyday activity where people try to beat their best. Ideas: skipping, holding a plank, stacking cups, a memory game. On paper, design an Arduino gadget that times or counts it and remembers the best.
Your design must include:
- What is measured, and whether lower or higher is better.
- The inputs (buttons, sensors) and outputs (LEDs, buzzer, Serial or LCD).
- The EEPROM address and type for the best score.
- The value that means "no score yet", and the gesture that resets it.
Part 2 — Make it
Build it with buttons and LEDs from your kit. Save the best with EEPROM.put() only when it is beaten. Unplug and replug the board, then check the best is still there.
Bring back next class: your design, your hw-l02-40.ino sketch, and a Serial Monitor screenshot of the best score after a power cut.