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.
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:
| Knob | Reading | BPM | Period |
|---|---|---|---|
| all the way down | 0 | 60 | 1000 ms |
| a quarter | 256 | 90 | about 667 ms |
| halfway | 512 | 120 | 500 ms |
| all the way up | 1023 | 180 | about 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.
“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.
“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 neededPractice questions
Three quick questions from the lessons. Pick an answer to check it.
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.
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.
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.
“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.
Related topics
- millis() vs delay() on ArduinoWhy delay() freezes your sketch, and how millis() runs several jobs at once.
- How the Arduino map() function worksRescale a reading from one range to another, and the traps that come with it.
- 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.
- How to blink an LED without delay() on ArduinoBlink an LED with millis() so loop() stays free for every other job.