An Arduino button counter that counts each press once
Intermediate
Or see every lesson.
- 455parts on the bench
- 43boards running now
- 1.00xreal time, on every board
To count button presses, count the moment the button goes down, not every time loop() finds it down. Keep what the button was last time around; when it is down now and was up before, that is one press. Then wait out the few milliseconds of contact bounce before believing a change, or one press counts as several.
The circuit above is a scoreboard on an Uno: a MAX7219 eight-digit display, with the home score on the left, a game clock in the middle and the guest score on the right. Press Run, then the home or guest button, and that score goes up by one. Hold a button for a second and the score goes back down one, for a point given by mistake. The third button starts and stops the clock.
Below is the counter built up from the mistake everyone makes first, with the counts each version gave for the same two presses on an Uno, a long press that clears the count, and two buttons that count up and down.
Counting while it is held
The first try adds one whenever the pin reads LOW. loop() runs tens of thousands of times a second, so one press counted 22977 in the first 200 ms it was held, and the next reading was -30752: an int cannot go past 32767, so it wrapped around to the negative numbers. The button is wired from pin 2 to GND, with INPUT_PULLUP, so pressed is LOW.
int count = 0;
void setup() {
Serial.begin(9600);
pinMode(2, INPUT_PULLUP);
}
void loop() {
if (digitalRead(2) == LOW) {
count++;
}
static unsigned long last = 0;
if (millis() - last >= 500) {
last = millis();
Serial.println(count);
}
}State change detection
Keep the reading from last time and count only the change from up to down. That is state change detection, or edge detection. Two presses now counted 10, not tens of thousands, but still not 2.
The other 8 are bounce. A button’s metal contacts spring apart and touch again several times in the first few milliseconds of a press and of a release, and the sketch is fast enough to see each touch as a new press. Mokxi’s pushbutton bounces like a real one, which is why it shows up here.
const int BUTTON = 2;
int count = 0;
bool wasDown = false;
void setup() {
Serial.begin(9600);
pinMode(BUTTON, INPUT_PULLUP);
}
void loop() {
bool down = digitalRead(BUTTON) == LOW;
if (down && !wasDown) {
count++;
Serial.print("presses: ");
Serial.println(count);
}
wasDown = down;
}Debounced: one press, one count
Only believe the button once it has read the same for a while. The sketch below notes when the reading last changed and accepts the new state only after 20 ms without a change. The same two presses printed "presses: 1" and "presses: 2". No delay() is needed, so the rest of loop() keeps running.
const int BUTTON = 2;
int count = 0;
bool stable = false;
bool lastRead = false;
unsigned long changedAt = 0;
void setup() {
Serial.begin(9600);
pinMode(BUTTON, INPUT_PULLUP);
}
void loop() {
bool reading = digitalRead(BUTTON) == LOW;
if (reading != lastRead) {
changedAt = millis();
lastRead = reading;
}
if (millis() - changedAt > 20 && reading != stable) {
stable = reading;
if (stable) {
count++;
Serial.print("presses: ");
Serial.println(count);
}
}
}A long press that clears the count
To tell a press from a hold, note when the button went down and count on the way up instead. If it was held for a second, do the hold’s job and skip the count. With two short presses and then a long one, a version of the sketch above built this way printed "presses: 1", "presses: 2" and "held: back to 0".
The scoreboard running above does exactly this in its check() function: a release after at least 20 ms is a press, and a hold of 1000 ms is a hold, which takes a point back.
uint8_t check(Button &b) {
bool down = digitalRead(b.pin) == LOW;
unsigned long now = millis();
uint8_t event = 0;
if (down && !b.down) {
b.since = now;
b.held = false;
} else if (down && !b.held && now - b.since >= 1000) {
b.held = true;
event = 2;
} else if (!down && b.down && !b.held && now - b.since >= 20) {
event = 1;
}
b.down = down;
return event;
}Why not count with an interrupt?
attachInterrupt() on pin 2 runs a function the instant the pin changes, which sounds made for counting. For a button it is the wrong tool: the interrupt sees every bounce too, so it counts too many just as the state change sketch did, and debouncing inside an interrupt handler means timing with millis() there, which is fiddly. An interrupt earns its place for pulses too fast or too short for loop() to catch, such as a flow sensor or a wheel encoder, and the interrupts page has a counter for those.
A press lasts tens of milliseconds at the shortest and loop() comes around thousands of times in that, so reading the pin in loop() never misses one, as long as nothing in loop() blocks for long. A delay(1000) somewhere else in the sketch is what makes a counter miss presses.
Keep the count after a reset
A variable starts again from 0 every time the board resets or loses power. To keep a count, save it with EEPROM.put(0, count) when it changes and read it back with EEPROM.get(0, count) in setup(). The EEPROM wears out after about 100,000 writes to one address, so for a counter that changes often, save it every so many presses or when a button says so, not on every press.
Up and down with two buttons
Two buttons, two remembered states. The sketch below counts up on pin 2 and down on pin 3 and keeps the count between 0 and 99 with constrain(). One press of down, two of up and one of down printed 0, 1, 2 and 1: the first press could not go below 0. A short delay(20) at the end of loop() is the simplest debounce when the sketch has nothing else to do.
From here, show the count on a display: the seven-segment, TM1637 and LCD pages each print a number. On an Uno, an int counts to 32767; use an unsigned long for more.
const int UP = 2;
const int DOWN = 3;
int count = 0;
bool upWas = false;
bool downWas = false;
void setup() {
Serial.begin(9600);
pinMode(UP, INPUT_PULLUP);
pinMode(DOWN, INPUT_PULLUP);
}
void loop() {
bool up = digitalRead(UP) == LOW;
bool down = digitalRead(DOWN) == LOW;
if (up && !upWas) count++;
if (down && !downWas) count--;
if ((up && !upWas) || (down && !downWas)) {
count = constrain(count, 0, 99);
Serial.println(count);
}
upWas = up;
downWas = down;
delay(20);
}Questions
How do I count button presses on an Arduino?
Remember the button’s state from the last pass through loop() and add one only when it changes from up to down. Debounce it, or one press counts several times.
Why does my button counter count more than once per press?
Contact bounce: the button makes and breaks contact several times in the first few milliseconds. Accept a change only after the reading has held for about 20 ms.
Why does my Arduino counter go negative?
It is counting every pass of loop() while the button is held, and an int wraps from 32767 to -32768. Count the change from up to down instead.
How do I detect a long press on an Arduino button?
Save millis() when the button goes down. If it is still down a second later, it is a long press; if it comes up sooner, it was a short one.
Keep going
Where the facts on this page come from
Build this for real
Open the editor, change a value and watch the number move with it. Nothing to install, and no account needed.