Who is the fastest runner in your class? How long does a paper helicopter take to fall? A stopwatch answers those questions. This one shows the time on the 4-digit display, in seconds and hundredths of a second for the first 100 seconds, and then in minutes and seconds. The big button starts and stops it. The second button takes a lap time while it is running (the display holds the time for three seconds while the clock keeps counting in the background), and resets to zero when it is stopped.
What you'll learn
- How to measure time precisely with millis()
- How to keep a display bright by refreshing it all the time
- How to tell a new button press from a held button
- How to show the same time in two different formats
What you need
- UNO R3 board and USB cable (UNO R3 Starter Kit)
- 4-digit 7-segment display, common cathode (UNO R3 Starter Kit)
- 4 resistors, 220 Ω (UNO R3 Starter Kit)
- 2 push buttons (UNO R3 Starter Kit)
- Passive buzzer (UNO R3 Starter Kit)
- The printed parts from the 3D printing box below (stopwatch case), or the cardboard version described there
- Breadboard and jumper wires (UNO R3 Starter Kit)
Wiring

| From | To |
|---|---|
| Display segments a, b, c, d, e, f, g, dp | UNO pins 2, 3, 4, 5, 6, 7, 8, 9 |
| Display digits D1, D2, D3, D4 | 220 Ω resistor each, then UNO pins 10, 11, 12, 13 |
| Start/stop button, one leg | UNO pin A0 |
| Lap/reset button, one leg | UNO pin A1 |
| Both buttons, the diagonally opposite leg | GND |
| Passive buzzer + | UNO pin A2 |
| Passive buzzer − | GND |
The display is wired exactly as in the dice lesson, which also explains how the 12 pins work.
The sketch
const int SEG_PINS[8] = {2, 3, 4, 5, 6, 7, 8, 9}; // a, b, c, d, e, f, g, dp
const int DIGIT_PINS[4] = {10, 11, 12, 13};
const byte NUMBERS[10] = {0x3F, 0x06, 0x5B, 0x4F, 0x66, 0x6D, 0x7D, 0x07, 0x7F, 0x6F};
const byte DOT = 0x80;
const int START_PIN = A0, LAP_PIN = A1, BUZZER_PIN = A2;
bool running = false;
unsigned long startedAt = 0; // when the current run started
unsigned long saved = 0; // time counted before the last stop
unsigned long lapTime = 0, lapShownUntil = 0;
int laps = 0;
byte shown[4];
int current = 0;
unsigned long lastSwitch = 0;
unsigned long elapsed() {
return saved + (running ? millis() - startedAt : 0);
}
// Light one digit every 2 ms, without ever waiting
void refresh() {
if (micros() - lastSwitch < 2000) return;
lastSwitch = micros();
digitalWrite(DIGIT_PINS[current], HIGH);
current = (current + 1) % 4;
for (int s = 0; s < 8; s++) digitalWrite(SEG_PINS[s], bitRead(shown[current], s));
digitalWrite(DIGIT_PINS[current], LOW);
}
void showTime(unsigned long ms) {
if (ms < 100000) { // under 100 s: seconds.hundredths
unsigned long h = ms / 10;
shown[0] = h >= 1000 ? NUMBERS[h / 1000 % 10] : 0; // no leading zero
shown[1] = NUMBERS[h / 100 % 10] | DOT;
shown[2] = NUMBERS[h / 10 % 10];
shown[3] = NUMBERS[h % 10];
} else { // then minutes.seconds
unsigned long s = ms / 1000;
unsigned long m = min(s / 60, 99UL);
shown[0] = NUMBERS[m / 10];
shown[1] = NUMBERS[m % 10] | DOT;
shown[2] = NUMBERS[s % 60 / 10];
shown[3] = NUMBERS[s % 10];
}
}
// true once per press: when the button goes from up to down
bool pressed(int pin, bool& wasDown, unsigned long& last) {
bool down = digitalRead(pin) == LOW;
bool now = down && !wasDown && millis() - last > 50;
if (down != wasDown) last = millis();
wasDown = down;
return now;
}
void printTime(const char* label, unsigned long ms) {
Serial.print(label);
Serial.print(ms / 1000.0, 2);
Serial.println(" s");
}
void setup() {
Serial.begin(9600);
for (int s = 0; s < 8; s++) pinMode(SEG_PINS[s], OUTPUT);
for (int d = 0; d < 4; d++) {
pinMode(DIGIT_PINS[d], OUTPUT);
digitalWrite(DIGIT_PINS[d], HIGH);
}
pinMode(START_PIN, INPUT_PULLUP);
pinMode(LAP_PIN, INPUT_PULLUP);
}
void loop() {
static bool startDown = false, lapDown = false;
static unsigned long startLast = 0, lapLast = 0;
if (pressed(START_PIN, startDown, startLast)) {
if (running) {
saved = elapsed();
running = false;
printTime("Stopped at ", saved);
tone(BUZZER_PIN, 1200, 60);
} else {
startedAt = millis();
running = true;
tone(BUZZER_PIN, 2000, 60);
}
}
if (pressed(LAP_PIN, lapDown, lapLast)) {
if (running) { // lap: hold the time on the display for 3 s
lapTime = elapsed();
lapShownUntil = millis() + 3000;
laps++;
Serial.print("Lap ");
Serial.print(laps);
printTime(": ", lapTime);
tone(BUZZER_PIN, 2600, 40);
} else { // stopped: back to zero
saved = 0;
laps = 0;
tone(BUZZER_PIN, 600, 120);
}
}
showTime(millis() < lapShownUntil ? lapTime : elapsed());
refresh();
}
Try it
- After uploading, the display shows
0.00. Press the start button: the hundredths fly by. - Press the lap button while it runs: the time freezes for three seconds, then jumps to the running time. The Serial Monitor (9600 baud) lists every lap.
- Press start again to stop, and the lap button to reset. Let it run for more than 100 seconds to see it switch to minutes and seconds.
How it works
millis() counts the milliseconds since the UNO started. The stopwatch never counts on its own: when you press start, it notes the time in startedAt, and the elapsed time is simply "now minus then". When you stop, the time so far goes into saved, so the next start carries on from there.
The display can only light one digit at a time. refresh() moves to the next digit every 2 ms, so all four flash 125 times a second and look steady. It never uses delay(), otherwise the display would flicker while the sketch waits.
To show 12.34 seconds, the sketch turns 12,345 ms into 1,234 hundredths and picks the digits out with / and %: 1234 / 100 % 10 is 2, the digit before the dot. The dot itself is the extra bit 0x80, added with |.
pressed() only says "yes" at the moment a button goes down, not while you hold it, and ignores the tiny bounces in the first 50 ms.
Try this next
- Add a countdown mode: hold the lap button at start to set 10, 30 or 60 seconds.
- Measure your reaction time: the stopwatch starts by itself after a random wait, and you stop it as fast as you can.
- Keep the best lap and show it when you press both buttons at once.
Troubleshooting
- Some segments are dark or wrong: check the eight segment wires against the table and your display's datasheet.
- A button does nothing: use the diagonally opposite legs of the button, one to the pin and one to GND.
-
The display flickers: make sure there is no
delay()inloop(). - A printed part is too tight or too loose: printers differ by a few tenths of a millimetre. Sand or file a tight hole, and fix a loose part with a drop of hot glue or a strip of tape.