Embedded Systems
How to make a stopwatch with Arduino and millis()
An Arduino stopwatch is two glances at the clock and a subtraction. When it starts, save millis() in a variable; when it stops, take that away from millis() again. The difference is the time in milliseconds: elapsed = millis() − startedAt. A bool remembers whether it's running, so one button can start and stop it.
Why the start must be saved
millis() counts the milliseconds since the board powered up, and it never pauses. By the time you press stop, it has moved on, so the start has to be written down, like the time stamped on a car park ticket. That's startedAt, declared above setup() so it survives every pass of loop().
Live experiment
Stop it at exactly 2.0 seconds
One press starts the stopwatch and writes millis() on its ticket, startedAt; the next stops it and prints the time in tenths of a second. Try to stop it at exactly 2.0 s. Dividing by 100 throws away what's left over, so anything from 2000 to 2099 ms counts.
“Stopwatch” is lesson 4 of 6 in Clockwork Cove, Embedded Systems World 2. Free with an account. Lessons unlock in order, so you’ll do the 10 before it first.
The sketch
A button from pin 2 to GND, read with INPUT_PULLUP. Each press is caught once, on the change from HIGH to LOW, ignoring the pin for 50 ms after each change while the contacts bounce:
bool running = false;
unsigned long startedAt = 0;
bool last = HIGH;
unsigned long changedAt = 0;
void setup() {
pinMode(2, INPUT_PULLUP);
Serial.begin(9600);
}
void loop() {
bool now = digitalRead(2);
if (now != last && millis() - changedAt > 50) {
changedAt = millis();
last = now;
if (now == LOW) { // a new press
if (!running) {
startedAt = millis(); // start: note the time
} else {
unsigned long elapsed = millis() - startedAt;
Serial.print("tenths: ");
Serial.println(elapsed / 100);
}
running = !running; // flip the state
}
}
}
The first press starts it, the second stops it and prints, the third starts again. running = !running flips the state, true to false or false to true, in one line.
Printing it nicely
Milliseconds are hard to read, so the sketch divides by 100 to print tenths of a second. Both numbers are whole, so the division drops what's left over: 1250 ms prints 12, and 1980 ms prints 19, not 20.
Serial.print writes without ending the line and Serial.println ends it, so a label and its number share one line: tenths: 12. For seconds with decimals, divide by 1000.0 and choose the places: Serial.println(elapsed / 1000.0, 2) prints 1.25.
Make it a game
The same two glances time anything: a reaction-time game that lights an LED after a random wait and times how fast you press, a lap timer, or how long a door was left open.
Free lesson
Now build it yourself
Each lesson comes in three short parts: see the idea, try it on a live board, then build it yourself, with quick checks as you go.
“Stopwatch” is lesson 4 of 6 in Clockwork Cove, Embedded Systems World 2. Free with an account. Lessons unlock in order, so you’ll do the 10 before it first.
Or start from the very first lesson, no account neededPractice questions
Three quick questions from the lessons. Pick an answer to check it.
Show the answer
Answer: millis() keeps moving, so the start would be lost by the time you stop. The clock never pauses. You have to write down where it was at the start, like a parking ticket.
- Why not “To make it faster”? Saving it doesn't speed anything up. It keeps the start time, which millis() itself won't.
- Why not “It doesn't matter”? It matters: by the stop, millis() has moved on, so without a saved start there's nothing to measure from.
- Why not “Serial needs it”? Serial only prints the result. The start is saved so there's something to subtract at the stop.
Show the answer
Answer: tenths: 19. 3480 − 1500 = 1980 ms. 1980 / 100 is 19.8, and dividing whole numbers drops what's after the point: tenths: 19.
- Why not “tenths: 1980”? 1980 is the elapsed milliseconds before dividing. The sketch prints it divided by 100.
- Why not “tenths: 20”? Whole-number division drops the remainder instead of rounding, so 19.8 becomes 19, not 20.
- Why not “tenths: 19.8”? Both numbers are whole numbers, so the division throws away the .8 instead of printing it.
Show the answer
Answer: flips running between true and false. ! means NOT. If running was false it becomes true, and the next time, false again. One line flips the stopwatch's state.
- Why not “waits until running is false”? Nothing waits here. The line flips the value once and the program carries on.
- Why not “sets running to true”? It sets true only when running was false. When running is true, the same line makes it false.
- Why not “nothing”? It changes running every time it runs, from true to false or from false to true.
“Stopwatch” is lesson 4 of 6 in Clockwork Cove, Embedded Systems World 2. Free with an account. Lessons unlock in order, so you’ll do the 10 before it first.
Questions people ask
How accurate is a millis() stopwatch?
It counts whole milliseconds, so each reading can be a millisecond or two out, and the board's clock crystal or resonator drifts by a small fraction of a percent. For finer timing, micros() counts in steps of 4 µs on a 16 MHz Uno.
What happens when millis() rolls over?
millis() is an unsigned long, so it wraps to 0 after about 49.7 days. If you store times as unsigned long and always subtract, now minus start, the answer still comes out right across the wrap.
How do I print the time in seconds with decimals?
Divide by 1000.0, with the decimal point, so the sum keeps its fraction, and tell println how many places to show: Serial.println(elapsed / 1000.0, 2) prints 1250 ms as 1.25.
Related topics
- millis() vs delay() on ArduinoWhy delay() freezes your sketch, and how millis() runs several jobs at once.
- How to blink an LED without delay() on ArduinoBlink an LED with millis() so loop() stays free for every other job.
- How to count button presses on an ArduinoCount the change, not the hold, keep the count above setup(), and use % for every Nth press.
- How to do two things at once on an ArduinoNo threads, no waiting: a loop() that glances at every job thousands of times a second.