Leer un pulsador bien: pull-up, antirrebote e interrupciones

Cómo conectar un pulsador al ESP32 sin dolores de cabeza: pull-up interno, por qué rebota, antirrebote por software y por hardware, e interrupciones bien hechas.

También disponible en: EN, ES

Serie Introducción al ESP32 Parte 20 Ver la serie →
Pulsador conectado a un ESP32

Leer un botón parece lo más fácil del mundo: digitalRead() y ya. Y luego pasa lo de siempre: una pulsación cuenta como cinco, o el pin lee ruido aleatorio cuando nadie toca nada. Ambas cosas tienen explicación y solución.

Primero: nunca dejes un pin «al aire»

Si conectas un botón entre el pin y 3V3 sin nada más, cuando no está pulsado el pin queda flotando y lee basura. Necesitas una resistencia que lo lleve a un nivel conocido:

  • Pull-up: el pin a HIGH en reposo; al pulsar, a GND. Es lo que uso casi siempre.
  • Pull-down: el pin a LOW en reposo; al pulsar, a 3V3.

El ESP32 tiene pull-up interno, así que normalmente no hace falta resistencia:

constexpr int BTN = 4;

void setup() {
  Serial.begin(115200);
  pinMode(BTN, INPUT_PULLUP);   // reposo = HIGH, pulsado = LOW
}

void loop() {
  if (digitalRead(BTN) == LOW) {
    Serial.println("pulsado");
  }
}

Ojo: los GPIO34–39 no tienen pull-up interno (son solo entrada). Si usas uno de esos, necesitas resistencia externa.

Por qué «rebota» (y por qué ves 5 pulsaciones)

Un pulsador no pasa de HIGH a LOW limpiamente. Durante unos milisegundos hace contacto y lo pierde varias veces. El ESP32, que va a 240 MHz, lo detecta todo. Eso es el bounce.

Antirrebote por software (mi favorito)

constexpr int BTN = 4;
constexpr unsigned long DEBOUNCE_MS = 30;

bool estable = HIGH;
bool ultimo = HIGH;
unsigned long cambio = 0;

void loop() {
  bool lectura = digitalRead(BTN);
  if (lectura != ultimo) {
    cambio = millis();
    ultimo = lectura;
  }
  if (millis() - cambio > DEBOUNCE_MS && lectura != estable) {
    estable = lectura;
    if (estable == LOW) Serial.println("pulsación válida");
  }
}

La idea: solo aceptas un cambio si se mantiene estable 30 ms. Es suficiente para el 99 % de los botones.

Antirrebote por hardware

Si el botón es crítico o hay mucho ruido, un RC (10 kΩ + 100 nF) y, si hace falta, un Schmitt trigger (74HC14) lo arreglan. Más piezas, pero cero código.

Interrupciones: potentes y peligrosas

Con attachInterrupt() el ESP32 ejecuta tu función al instante. Es ideal para no perder pulsaciones, pero tiene reglas:

volatile bool pulsado = false;

void IRAM_ATTR alPulsar() {   // IRAM_ATTR: debe vivir en IRAM
  pulsado = true;
}

void setup() {
  pinMode(BTN, INPUT_PULLUP);
  attachInterrupt(digitalPinToInterrupt(BTN), alPulsar, FALLING);
}

void loop() {
  if (pulsado) {
    pulsado = false;
    Serial.println("interrupción");
  }
}

Reglas de oro:

  • La variable compartida debe ser volatile.
  • La función debe llevar IRAM_ATTR (si no, puede fallar con el Wi-Fi).
  • No hagas trabajo pesado dentro de la ISR: marca una bandera y procésala en el loop(). Nada de Serial.print ni delay ahí dentro.
  • Aun con interrupción, el rebote existe: añade antirrebote igualmente.

Errores típicos

  1. Botón sin pull-up y pines leyendo ruido → crees que el ESP32 está roto.
  2. Usar GPIO34–39 para un botón y descubrir que no hay pull-up interno.
  3. Poner Serial.println dentro de la ISR → cuelgues aleatorios.
  4. Olvidar volatile → el compilador «optimiza» y la bandera nunca cambia.
  5. No hacer antirrebote y contar 5 pulsaciones por cada una.

Resumen

  • Nunca dejes un pin de entrada flotando: usa INPUT_PULLUP.
  • Los botones rebotan; con 30 ms de antirrebote se soluciona.
  • Interrupciones: volatile, IRAM_ATTR, trabajo mínimo y antirrebote también.
  • GPI34–39 no tienen pull-up: resistencia externa obligatoria.
  • Con hardware (RC + Schmitt) te ahorras código si el ruido es serio.

Siguiente paso: monitor serie y depuración básica, para dejar de adivinar por qué no funciona. O vuelve al hub de la serie.

Entradas relacionadas

↑↓ navegar ↵ abrir Esc cerrar