Learning Goals 5 min
By the end of this lesson you will be able to:
- Use
millis()to take two timestamps and compute the elapsed milliseconds between them. - Use
random(min, max)to pick an unpredictable wait between rounds, and use theunsigned longdata type for numbers too big for a normalint. - Write a minimal
whileloop that waits for a button press — and combine it withmillis()to build a working reaction-timer game.
Warm-Up 10 min
The L01-18 🔴 stretch task gave you a sneak peek of this lesson: a reflex game that times how fast you press the button after a random light. Today we build it properly — including the three new pieces of the language it needs.
Quick-fire puzzle
Arjun sits at his Arduino with a button under his finger. The onboard LED is dark. After a random pause of "somewhere between 2 and 5 seconds", the LED comes on. He presses the button as fast as he can. The Serial Monitor prints 187 ms.
- How did the Arduino know how long Arjun took to press? (Hint: it must have looked at some kind of clock before the press and after.)
- How did the Arduino pick "somewhere between 2 and 5 seconds" as the random pause? Did it roll dice?
- While the Arduino was waiting for Arjun's press, what was it doing in
loop()— was it sleeping, or repeatedly checking?
Reveal the answer
- The Arduino has an internal millisecond counter that starts ticking from zero the moment the board powers up. The sketch reads this counter before the LED came on, saves it, reads it again after the press, and subtracts. The difference is the reaction time. The function that returns the counter is
millis(). - The Arduino has a built-in pseudo-random number generator. The function
random(2000, 5000)returns a different "random" integer each time you call it — between 2000 (inclusive) and 5000 (exclusive). Not real dice — a deterministic algorithm — but good enough for games. - Repeatedly checking. It sat in a tiny "
whileloop" — a kind of loop you haven't met yet — that did nothing except look at the button over and over until it saw a press. We'll meetwhiletoday as the simplest of the three new pieces.
New Concept 20 min
The big idea — a timer needs time, a game needs randomness, a wait needs a loop
Today's game has three small pieces of language to learn at once:
millis()— gives you the current "clock" reading.random(min, max)— gives you an unpredictable integer.while— runs a body repeatedly, as long as a condition is true.
The first two are function calls; the third is a control structure like for or if. Each one alone is small. Glue them together and you have a game.
millis() — the millisecond clock
The moment the Arduino powers up, an internal counter starts ticking. Every millisecond it adds 1. millis() returns the current value. Call it twice with something between, and the difference is how long that "something" took.
unsigned long start = millis();
// ... something happens ...
unsigned long elapsedMs = millis() - start;Why unsigned long?
The millisecond counter can grow very large. After just 25 days of running, it reaches over 2 billion — more than a normal int can hold. (An int on the UNO maxes out at 32,767.) So we use a wider type: unsigned long, which holds positive numbers up to about 4.3 billion. The same type that millis() itself returns.
| Type | Range on the UNO | When to use |
|---|---|---|
int | −32,768 to +32,767 | Pin numbers, small counters, anything that fits. |
unsigned int | 0 to 65,535 | Twice the positive range — rare outside specialised work. |
long | −2.1 billion to +2.1 billion | Big signed numbers. |
unsigned long | 0 to 4.3 billion | Anything from millis() or micros(). Always use this for time values. |
random(min, max) — pseudo-random integers
random(2000, 5000) returns a value somewhere in the range 2000 (inclusive) to 5000 (exclusive) — that is, 2000, 2001, …, 4999. Each call gives a different result. Calls are independent — knowing the previous values doesn't help you predict the next.
A small honest caveat: random isn't truly random. It uses a fixed mathematical formula starting from a hidden "seed", and without setting the seed, your Arduino plays the same sequence after every restart. For a reaction-timer game where the player doesn't see the wait times, this is fine. To get a different sequence each boot, you'd add randomSeed(analogRead(A0)) in setup() — but analog reads are a Level 2 topic, so we'll leave that for later.
while loops — repeat until a condition flips
while is the simplest of the three loop kinds (the others are for from L01-11 and do-while later). It checks a condition; if true, runs the body; checks again; repeats. The moment the condition becomes false, the loop exits.
while (digitalRead(BUTTON_PIN) == HIGH) {
// body — runs over and over while the button is still released
}
// The moment the button is pressed, digitalRead returns LOW,
// the condition becomes false, and execution falls past the loop.Today we use while with an empty body — the sketch just spins, checking the button thousands of times per second, until the press happens. The Arduino can't do anything else during this wait (this is called blocking), but for a one-thing-at-a-time reflex game that's exactly what we want.
The pattern — snapshot, wait, snapshot, subtract
unsigned long start = millis(); // snapshot before
while (digitalRead(BUTTON_PIN) == HIGH) { } // wait for press
unsigned long reactionMs = millis() - start; // snapshot after, subtractThis is the canonical "how long did this take" pattern in every Arduino sketch ever written. Memorise the shape; you'll use it constantly.
Reuse the L01-21 wiring (mostly)
Today's game needs just one LED (we'll use the red one on D9 — the "go" signal) and one button (button A on D7). The other LEDs and the second button can stay wired from L01-21 — they just won't do anything today. No new wires.
Why it matters
Time is half of everything in interactive electronics. "How long did the user hold the button?" "How long since the last sensor reading?" "How much time before the alarm sounds?" Every one of these is a millis() question. Today's three-piece kit — millis(), random, while — is enough to build a real, playable, share-with-your-friends reflex game in 15 lines of code.
Worked Example 20 min
Goal: build a complete reaction timer where the red LED lights up at a random moment, the player presses the button, and the reaction time prints to Serial.
The sketch
const int LED_PIN = 9;
const int BUTTON_PIN = 7;
void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
delay(random(2000, 5000));
digitalWrite(LED_PIN, HIGH);
unsigned long start = millis();
while (digitalRead(BUTTON_PIN) == HIGH) { }
unsigned long reactionMs = millis() - start;
digitalWrite(LED_PIN, LOW);
Serial.print("Reaction: ");
Serial.print(reactionMs);
Serial.println(" ms");
delay(3000);
}Step-by-step trace
- Random wait.
delay(random(2000, 5000))pauses for somewhere between 2 and 5 seconds. The player doesn't know how long. - Lights on.
digitalWrite(LED_PIN, HIGH)snaps the red LED on — the "go" signal. - Snapshot before.
start = millis()records the exact moment the LED came on. - Spin and wait. The
whileloop keeps checkingdigitalRead(BUTTON_PIN). It returns HIGH (released) at first, so the loop body (empty) runs. And runs. And runs. Thousands of times a second. The moment the player presses, the read returns LOW, the condition becomes false, and the loop exits. - Snapshot after, subtract.
millis()is called again. The difference fromstartis the reaction time. - Lights off, print. Turn the LED off; print the result to Serial.
- Pause and repeat.
delay(3000)gives the player 3 seconds to rest before the next round.
Upload and play
- Upload the sketch.
- Open the Serial Monitor at 9600 baud.
- Stare at the red LED. Don't press until it lights.
- When it lights, press as fast as you can.
- Read your time in Serial. Try a few rounds.
- For reference: top sprinters in athletics start moving in about 140 ms. A casual press is usually 200–300 ms. Anything under 250 ms is excellent.
What if the player "cheats" by holding the button down before the LED lights?
Try it: hold the button down throughout, including the random wait. The moment the LED lights, the while condition is already false (button reads LOW). The loop exits immediately. reactionMs is something tiny, like 0 or 1 ms.
The Arduino can't tell whether you're a reflex superhuman or a cheater. We'll fix that in the 🟡 task below — add a "false start" detector.
Try It Yourself 20 min
Goal: Change the random wait range. Try a harder version where the LED comes on faster (1–2 seconds) and an easier version where it takes longer (5–10 seconds).
// Harder: shorter wait — your brain has less time to relax
delay(random(1000, 2000));
// Easier: longer wait — more anticipation, easier to overshoot
delay(random(5000, 10000));Questions:
- Which range gave you faster average reaction times — the short wait, the long wait, or the original 2–5 second range? ____ (Hint: most people are worse with very long waits because they relax mentally.)
- Why is
random(2000, 5000)generally a sweet spot for reflex games? ____
Goal: Add a "false start" detector. If the player presses the button before the LED lights, print "Too early!" and start the round over.
The trick: during the random wait, watch the button. Instead of a plain delay(), use a while loop with a millis() check that ALSO checks the button.
// Replace the delay(random(2000, 5000)) with this:
unsigned long waitUntil = millis() + random(2000, 5000);
while (millis() < waitUntil) {
if (digitalRead(BUTTON_PIN) == LOW) {
Serial.println("Too early!");
delay(2000);
return; // abort this loop pass; loop() restarts from the top
}
}Questions:
- The
while (millis() < waitUntil)pattern is the non-blocking way to wait, because the body can do other things (like check the button). Why isdelay()alone NOT enough for this task? ____ - The
returnkeyword exits the current function — here,loop(). What happens next? (Hint: think about what callsloop().) ____
Goal: Use all three LEDs to show how good your reaction time was. Red = bad (over 400 ms), yellow = okay (250–400 ms), green = great (under 250 ms). Combines today's lesson with the else if bands from L01-20.
// After computing reactionMs, light one of three LEDs
if (reactionMs < 250) {
digitalWrite(GREEN_PIN, HIGH);
}
else if (reactionMs < 400) {
digitalWrite(YELLOW_PIN, HIGH);
}
else {
digitalWrite(RED_PIN, HIGH);
}
delay(2000);
digitalWrite(GREEN_PIN, LOW);
digitalWrite(YELLOW_PIN, LOW);
digitalWrite(RED_PIN, LOW);Questions:
- Do you notice that which LED lights up gives you the answer faster than reading the Serial Monitor number? Why? (Hint: pattern recognition vs reading digits.) ____
- If you set the thresholds too generously (e.g., red only above 1000 ms), what would happen? ____
Mini-Challenge 15 min
The "best time" tracker
Add a memory to the game: after each round, the sketch keeps track of the best reaction time so far in a mutable global. After every round, Serial prints both this round's time and your best so far.
Your task:
- Add a mutable global:
unsigned long bestMs = 99999;— a sentinel value bigger than any real reaction. - After computing
reactionMs, compare tobestMs. - If the new time is better (smaller) than the current best, update
bestMs. - Print both numbers to Serial in a clear format.
It works if:
- Round 1: prints something like
This: 245 ms · Best: 245 ms. - Round 2 (slower): prints something like
This: 312 ms · Best: 245 ms— best unchanged. - Round 3 (faster): prints something like
This: 198 ms · Best: 198 ms— best updated. - After playing 10 rounds, the "best" only ever gets smaller, never larger.
Reveal one valid sketch
const int LED_PIN = 9;
const int BUTTON_PIN = 7;
unsigned long bestMs = 99999;
void setup() {
pinMode(LED_PIN, OUTPUT);
pinMode(BUTTON_PIN, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
delay(random(2000, 5000));
digitalWrite(LED_PIN, HIGH);
unsigned long start = millis();
while (digitalRead(BUTTON_PIN) == HIGH) { }
unsigned long reactionMs = millis() - start;
digitalWrite(LED_PIN, LOW);
if (reactionMs < bestMs) {
bestMs = reactionMs;
}
Serial.print("This: ");
Serial.print(reactionMs);
Serial.print(" ms · Best: ");
Serial.print(bestMs);
Serial.println(" ms");
delay(3000);
}bestMs is a mutable global initialised to 99999 — a value bigger than any real reaction time. The first round's time is guaranteed to be smaller, so it becomes the new best. From then on, only better times can overwrite it. The pattern (sentinel start value + "if better, update") is the same one used to track high scores, lowest temperatures, fastest laps, anywhere a "personal best" matters.
Recap 5 min
Time is measured with millis(), randomness with random(min, max), and waiting-for-something with a while loop. Snapshot a millisecond reading before an event, again after, subtract — that's how every "how long did this take" sketch works. Store time values in unsigned long so they don't overflow. A reflex game is genuinely just these three pieces plus the button-and-LED tools you already had.
- millis()
- Returns the number of milliseconds since the Arduino powered up. Type:
unsigned long. Counts up forever (until 49 days, when it rolls back to 0). - random(min, max)
- Returns a pseudo-random integer in the half-open range
[min, max)— min included, max excluded. Same sequence each reset unless you seed it. - unsigned long
- A 32-bit unsigned integer type. Holds 0 to about 4.3 billion. Use it for any number that comes from
millis()ormicros(). - while loop
- Runs a body repeatedly while a condition is true. Exits the moment the condition becomes false. Today we use it with an empty body to "spin-wait" for a button press.
- Spin-wait / busy-wait
- Sitting in a tight loop, repeatedly checking some condition, doing nothing else. Simple and useful when the program has nothing else to do — but blocking.
- Sentinel value
- An impossibly extreme starting value (like
99999for a "best time") that any real value will improve on. Used to bootstrap "track the best/worst" patterns.
Homework 5 min
Two-player reaction duel. Build a version where TWO players race to press their button first. Use button A (D7) for player A and button B (D6) for player B — both from your L01-21 wiring.
- Random wait, then the red LED lights up — the "go" signal.
whileloop waits for either button to be pressed (use||from L01-21).- After the loop exits, check which button was pressed and print the winner:
"Player A wins!"or"Player B wins!". - Also print both reaction times — you'll need to capture the moment one of them pressed, then quickly read both buttons to figure out which.
Hint for step 2:
while (digitalRead(BUTTON_A_PIN) == HIGH && digitalRead(BUTTON_B_PIN) == HIGH) { }
// exits as soon as EITHER button reads LOWDe Morgan-style thinking: "stay in the loop while BOTH buttons are released" is the same as "exit as soon as AT LEAST ONE is pressed". The && in the condition catches the "both still released" state.
Also: a design reflection on paper.
- What happens if both players press at exactly the same moment? Whose name will the sketch print? (Hint: re-read which button it checks first.) ____
- The Mini-Challenge tracked a "best time". For a two-player game, what would you track instead? Sketch a name and starting value for one mutable global. ____
- Your sketch uses
millis(). About how long after powering on wouldmillis()hit its maximum value of ~4.3 billion ms? Convert to days. ____ (Hint: 4.3 × 10⁹ ms ÷ 86,400,000 ms per day.)
Bring back next class:
- The saved
.inofile (call ittwo-player-duel). - A short phone video (15–20 seconds) showing two rounds — once player A wins, once player B wins.
- Your three design-reflection answers on a notebook page.
Heads up for next class: L01-23 "Simple Burglar Alarm" is Cluster C's project lesson — combine buttons, LEDs, the buzzer from L01-14 and today's millis() trick to build a tilt-switch alarm that wails when triggered.