Skip to content

Embedded Systems

Build an Arduino metronome with millis()

An Arduino metronome turns a tempo into a time between beats, 60,000 ÷ BPM milliseconds, and flashes an LED at the start of each one. A knob on A0 sets the tempo, a button starts and stops it, and millis() does the timing, so the knob and the button are read on every pass and the beat never stutters.

2345678910111213A0A1A2A3A4A55VGNDLLearnEE Corea 50 ms flash each beat5 Vbutton: pin 2LED: pin 9knob: A060 to 180 beats a minute
The metronome: a button to start and stop it, a knob for the tempo, an LED that flashes on every beat

The parts

  • A push button from pin 2 to GND, read with INPUT_PULLUP
  • A 10 kΩ potentiometer between 5V and GND, its wiper on A0
  • An LED on pin 9 through 220 Ω, flashing for 50 ms on every beat

From knob to beat

map() turns the knob's 0 to 1023 into 60 to 180 beats a minute, and a minute is 60,000 ms:

period = 60,000 ms over BPM
KnobReadingBPMPeriod
all the way down0601000 ms
a quarter25690about 667 ms
halfway512120500 ms
all the way up1023180about 333 ms

map() rounds down: 512 × 120 ÷ 1023 is 60.06, so 512 gives 60 + 60 = 120 BPM, two beats a second.

Live experiment

Tick, tock

A press starts the metronome with a beat straight away; the next press stops it. The knob reads 0 to 1023 and map() makes that 60 to 180 beats a minute. Pip flashes for the first 50 ms of every beat while the arm swings. Turn the tempo up and watch the sums.

Try it yourself: free lesson

“Project: Metronome” is the project that ends Clockwork Cove, Embedded Systems World 2. Free with an account. Lessons unlock in order, so you’ll do the 13 before it first.

The sketch

bool running = false;
unsigned long lastBeat = 0;
bool last = HIGH;
unsigned long changedAt = 0;

void setup() {
  pinMode(2, INPUT_PULLUP);
  pinMode(9, OUTPUT);
}

void loop() {
  // the button: start and stop
  bool now = digitalRead(2);
  if (now != last && millis() - changedAt > 50) {
    changedAt = millis();
    last = now;
    if (now == LOW) {
      running = !running;
      lastBeat = millis();            // a beat right now
    }
  }

  // the tempo
  int bpm = map(analogRead(A0), 0, 1023, 60, 180);
  unsigned long period = 60000UL / bpm;

  // the beat
  if (running && millis() - lastBeat >= period) {
    lastBeat = lastBeat + period;
  }

  // the flash: the first 50 ms of each beat
  digitalWrite(9, running && millis() - lastBeat < 50);
}

Why it keeps good time

Each beat is moved on by exactly one period, lastBeat + period, not reset to whenever the code noticed it. So small delays in one pass don't add up, and the beats stay on a steady grid. The flash is worked out fresh on every pass from the time since the beat: HIGH for the first 50 ms, LOW for the rest.

Add a click and an accent

For sound, wire a passive piezo buzzer from pin 8 to GND and call tone(8, 1000, 20) where the beat moves on: a 20 ms click at 1 kHz that plays in the background. To stress the first beat of every bar of four, count the beats and check beats % 4 == 0, then play that click higher, at 2 kHz. Nothing else in the sketch changes.

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.

Try it yourself: free lesson

“Project: Metronome” is the project that ends Clockwork Cove, Embedded Systems World 2. Free with an account. Lessons unlock in order, so you’ll do the 13 before it first.

Or start from the very first lesson, no account needed

Practice questions

Three quick questions from the lessons. Pick an answer to check it.

  1. Question 1 of 3The flash should last 50 ms. Compared with the time since the beat started, the LED is HIGH while…
    Show the answer

    Answer: that time is less than 50. Keep the moment the beat started. While millis() minus that moment is under 50, write HIGH, otherwise LOW. No delay, so the button and the knob are still read every pass.

    • Why not “that time is more than 50”? More than 50 ms is the rest of the beat, so the LED would be on almost all the time and off only briefly.
    • Why not “the beat count is even”? Lighting on even beats would light every other beat for a whole beat, not flash briefly on each one.
    • Why not “the button is held”? The button starts and stops the metronome. The flash is timed from the beat, not from the button.
  2. Question 2 of 3To check whether 500 ms has passed, compare…
    Show the answer

    Answer: millis() − lastTime with 500. Now minus then.

    • Why not “lastTime with 500”? lastTime only changes when the job acts, so on its own it can't tell how long ago that was.
    • Why not “millis() with 500”? millis() alone passes 500 once, half a second after power-up, and stays above it forever after.
    • Why not “delay(500)”? delay(500) doesn't check anything. It stops the whole program for half a second.
  3. Question 3 of 3millis() counts…
    Show the answer

    Answer: Milliseconds since the board started. millis() is a counter that starts at 0 when the board powers up and ticks once every millisecond, like a clock on the wall.

    • Why not “How many times loop() has run”? loop() can run many times in a millisecond. millis() counts time, not passes.
    • Why not “Presses of the button”? Counting presses is your code's job. millis() just counts milliseconds since the board started.
    • Why not “Seconds until the next blink”? millis() counts up from power-on and knows nothing about blinks. You work out what's due from it.
Keep practising: free lesson

“Project: Metronome” is the project that ends Clockwork Cove, Embedded Systems World 2. Free with an account. Lessons unlock in order, so you’ll do the 13 before it first.

Questions people ask

How do I convert BPM to milliseconds?

Divide 60,000, the milliseconds in a minute, by the beats per minute. 120 BPM is 500 ms a beat, 60 BPM is 1000 ms and 90 BPM is about 667 ms.

How do I add a click sound?

Wire a passive piezo buzzer from a pin to GND and call tone(pin, 1000, 20) at each beat for a short 1 kHz click. tone() runs in the background, so the timing isn't held up.

Why does my metronome drift slowly out of time?

Setting lastBeat = millis() at each beat starts the next beat from whenever the code noticed, which is a little late every time, and the lateness adds up. Adding the period, lastBeat = lastBeat + period, keeps every beat on its grid.

Every topic