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.
Try It Yourself 15 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.
Hint
const int HIGH_ADDR = 0;
const int WORST_ADDR = 4;
unsigned long worstScore = 0; // sentinel: 0 = no score
EEPROM.get(WORST_ADDR, worstScore);
// after a successful reaction:
if (reaction > worstScore) {
worstScore = reaction;
EEPROM.put(WORST_ADDR, worstScore);
}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.
Hint
Use an array of 3 unsigned longs in RAM, saved as 12 contiguous bytes in EEPROM.
unsigned long scores[3];
EEPROM.get(0, scores); // get works on arrays too!
// After a reaction, find where (if anywhere) it fits:
for (int i = 0; i < 3; i++) {
if (reaction < scores[i]) {
// shift the lower scores down
for (int j = 2; j > i; j--) scores[j] = scores[j-1];
scores[i] = reaction;
EEPROM.put(0, scores);
break;
}
}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.
Hint
Row 0 = state message ("Press to start", "Wait...", "GO!", "Your time: 412"). Row 1 = always "Best: 412 ms". Use the template-once pattern and fixed-width formatting from L02-29. The arcade is suddenly screen-driven.
Mini-Challenge · 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.
Homework 5 min
Run a small tournament. Use the worked-example reaction timer (or your mini-challenge game) and challenge three friends or family members. For each:
- Reset the high score before they play.
- Let them play 10 attempts.
- Record their best time.
Then answer:
- Who won? By how much?
- Did anyone improve over their 10 attempts (i.e. their 10th attempt was faster than their 1st)? Why might that be?
- The reaction timer is genuinely fun for ~2 minutes. What single feature would you add to make it fun for 5 minutes? 30 minutes?
Bring back next class:
- Your tournament results table.
- Your three written answers.
- Tomorrow we step beyond the 1 KB limit with SD cards — proper file storage on Arduino.