Build a stopwatch with lap times on the 4-digit display

A pocket stopwatch for races and experiments: start and stop with one button, take lap times with the other. It counts hundredths of a second, then switches to minutes and seconds.
  • Level:Beginner
  • Duration: 50 min
  • Ages 10+
  • Platform: Arduino

Tools for this lesson

Helpful for this build. Open a tool to see tips and more projects that use it.

Build a stopwatch with lap times on the 4-digit display – EEMENTOR STEM™ lesson

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

Breadboard wiring diagram: Build a stopwatch with lap times on the 4-digit display
Breadboard view. Check each connection against the wiring table below.
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

  1. After uploading, the display shows 0.00. Press the start button: the hundredths fly by.
  2. 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.
  3. 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() in loop().
  • 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.

Build it yourself

Everything used in this lesson comes in one kit, with parts tested by our engineers.

UNO R3 Starter Kit | Arduino IDE Compatible | Free Tutorials $56.00

Shop the kit