Button Types and Terminology
Before writing a single line of code, understanding button mechanics prevents many common mistakes. The push buttons used on breadboards are typically momentary tactile switches: they connect the circuit only while you hold them. Two subtypes exist:
- Normally Open (NO): Circuit is open (disconnected) at rest, closes when pressed. This is the standard breadboard button.
- Normally Closed (NC): Circuit is closed at rest, opens when pressed. Used in safety applications (detecting a door opening breaks the circuit).
Most of this guide uses normally-open buttons, which are by far the most common in ESP32 projects.
Wiring a Button to ESP32 (Active-LOW)
The simplest and most robust approach uses INPUT_PULLUP — no extra resistor needed:
| Button Terminal | Connect To |
|---|---|
| Terminal A | ESP32 GPIO16 |
| Terminal B | ESP32 GND |
With INPUT_PULLUP the pin reads HIGH at rest (3.3V via internal resistor). Pressing the button shorts the pin to GND → reads LOW. This is active-LOW logic: LOW means pressed.
Basic Button Read — Complete Code
#define BUTTON_PIN 16
#define LED_PIN 2
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP); // HIGH at rest, LOW when pressed
pinMode(LED_PIN, OUTPUT);
Serial.println("Button demo — press the button!");
}
void loop() {
int state = digitalRead(BUTTON_PIN);
if (state == LOW) { // Active-LOW: LOW = pressed
digitalWrite(LED_PIN, HIGH);
Serial.println("PRESSED");
} else {
digitalWrite(LED_PIN, LOW);
// Serial.println("released"); // Comment out to reduce Serial spam
}
delay(20); // Small delay reduces Serial flood
}
Active-HIGH Wiring (Alternative)
If your circuit naturally drives the GPIO HIGH when triggered (like some sensors), use INPUT_PULLDOWN:
| Button Terminal | Connect To |
|---|---|
| Terminal A | ESP32 3.3V |
| Terminal B | ESP32 GPIO17 |
#define BUTTON_PIN 17
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLDOWN); // LOW at rest, HIGH when pressed
}
void loop() {
if (digitalRead(BUTTON_PIN) == HIGH) { // HIGH = pressed (active-HIGH)
Serial.println("PRESSED (active-HIGH)");
}
delay(20);
}
Toggle Button: On/Off Control
Many projects need a button that toggles a state rather than holding it active. The key is detecting only the falling edge (button going from HIGH to LOW):
#define BUTTON_PIN 16
#define LED_PIN 2
bool ledState = false;
bool lastState = HIGH;
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(LED_PIN, OUTPUT);
}
void loop() {
bool current = digitalRead(BUTTON_PIN);
// Detect falling edge: was HIGH, now LOW
if (lastState == HIGH && current == LOW) {
ledState = !ledState; // Toggle
digitalWrite(LED_PIN, ledState);
Serial.printf("LED toggled %sn", ledState ? "ON" : "OFF");
delay(50); // Simple debounce
}
lastState = current;
delay(10);
}
Detecting Short vs Long Press
#define BUTTON_PIN 16
#define LONG_MS 1000 // 1 second threshold
unsigned long pressStart = 0;
bool pressing = false;
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
}
void loop() {
bool current = (digitalRead(BUTTON_PIN) == LOW);
if (current && !pressing) { // Button just pressed
pressing = true;
pressStart = millis();
}
if (!current && pressing) { // Button just released
pressing = false;
unsigned long held = millis() - pressStart;
if (held >= LONG_MS) {
Serial.printf("LONG PRESS (%lu ms)n", held);
} else {
Serial.printf("SHORT PRESS (%lu ms)n", held);
}
}
delay(10);
}
Multiple Buttons with Array Management
const int BTNS[] = {16, 17, 18, 19};
const int LEDS[] = { 2, 13, 14, 27};
const int N = 4;
bool prevState[N];
void setup() {
Serial.begin(115200);
for (int i = 0; i < N; i++) {
pinMode(BTNS[i], INPUT_PULLUP);
pinMode(LEDS[i], OUTPUT);
prevState[i] = HIGH;
}
}
void loop() {
for (int i = 0; i < N; i++) {
bool cur = digitalRead(BTNS[i]);
if (prevState[i] == HIGH && cur == LOW) { // Falling edge
digitalWrite(LEDS[i], !digitalRead(LEDS[i])); // Toggle LED
Serial.printf("Button %d toggled LED %dn", i+1, i+1);
delay(50);
}
prevState[i] = cur;
}
delay(5);
}
Reading a Normally-Closed Button
NC buttons invert the logic. With INPUT_PULLUP and an NC button wired between GPIO and GND:
- Button at rest (NC = connected): Pin is shorted to GND → reads LOW (pressed appearance)
- Button pressed (NC opens): Pin pulled HIGH by resistor → reads HIGH (released appearance)
Simply invert your comparison: if (digitalRead(pin) == HIGH) to detect the NC button being pressed (opened).
Project: Morse Code Tapper
#define BUTTON_PIN 16
#define BUZZ_PIN 25
#define DOT_MS 100
#define DASH_MS 300
void setup() {
Serial.begin(115200);
pinMode(BUTTON_PIN, INPUT_PULLUP);
pinMode(BUZZ_PIN, OUTPUT);
}
void loop() {
if (digitalRead(BUTTON_PIN) == LOW) {
unsigned long start = millis();
tone(BUZZ_PIN, 800); // Start tone
while (digitalRead(BUTTON_PIN) == LOW); // Wait for release
tone(BUZZ_PIN, 0); // Stop tone
unsigned long held = millis() - start;
Serial.print(held >= DASH_MS ? "-" : ".");
delay(50);
}
}
Summary
Reading buttons on ESP32 boils down to: use INPUT_PULLUP with button wired to GND for the most reliable active-LOW setup, detect falling edges for clean toggle logic, measure press duration for short/long press differentiation, and add 50 ms of debounce delay to eliminate contact bounce. For more advanced debounce techniques, see the next guide on debouncing buttons.