Un botón que lee perfecto en la breadboard empieza a portarse mal apenas lo atornillás en una caja: pulsaciones fantasma, una luz que parpadea sola, un contador que salta de a tres en vez de uno. El código no cambió. Esto es el repaso de una sesión de depuración real para exactamente ese problema, en una Raspberry Pi 4 con un pulsador de panel en GPIO17.
El síntoma
La versión de la breadboard usaba esto, y funcionaba:
import RPi.GPIO as GPIO
import time
PIN = 17
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIN, GPIO.IN)
def button_callback(channel):
print(f"button_callback fired, GPIO{channel} = {GPIO.input(channel)}")
GPIO.add_event_detect(PIN, GPIO.FALLING, callback=button_callback, bouncetime=50)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
GPIO.cleanup()
Después de mover el botón a un panel con unos 40cm de cable plano hasta la Pi, el log para una única pulsación deliberada quedó así:
button_callback fired, GPIO17 = 0
button_callback fired, GPIO17 = 1
button_callback fired, GPIO17 = 0
button_callback fired, GPIO17 = 0
button_callback fired, GPIO17 = 1
button_callback fired, GPIO17 = 0
button_callback fired, GPIO17 = 1
Siete eventos para una pulsación, y algunos ocurren incluso con el botón en reposo. Esa segunda parte es la pista real: esto no es solo rebote del interruptor, algo anda mal con el estado de reposo del pin.
Sospechoso 1: una entrada flotante
GPIO.setup(PIN, GPIO.IN) sin el argumento pull_up_down deja el pin flotando cada vez que el botón está abierto. Una entrada flotante no es "0 hasta que se demuestre lo contrario": no tiene voltaje definido, y capta cualquier ruido cercano: fuentes conmutadas, el propio controlador PWM del ventilador de la Pi, cableado de corriente alterna detrás del panel. Un jumper corto de breadboard rara vez muestra este problema. Cuarenta centímetros de cable plano son suficientes para actuar como una pequeña antena.
La solución es configurar siempre una resistencia pull explícita, aunque el pin "parezca" leer bien sin ella durante el desarrollo:
GPIO.setup(PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
Con el pull-up interno activado, las lecturas en reposo dejaron de saltar entre 0 y 1. Pero una sola pulsación seguía disparando el callback tres o cuatro veces.
Puesta a tierra y recorrido del cableado
Las resistencias pull internas de la Raspberry Pi son débiles, unos 50k ohm. Eso alcanza para un tramo corto y bien apantallado, pero en un cable más largo que comparte recorrido con otro cableado, no basta para dominar el ruido inducido. Dos cambios resolvieron los glitches que quedaban en el estado de reposo:
- Reemplazar el pull-up interno por una resistencia externa de 10k entre 3.3V y el pin GPIO, físicamente cerca del botón.
- Llevar el cable de tierra del botón hasta la Pi por su propio conductor, en vez de compartir retorno con la tira de LEDs de la caja, que tiraba de una corriente nada despreciable.
Si tenés relés, motores o cualquier carga inductiva compartiendo la caja, mantené su cableado y su retorno físicamente separados del cableado GPIO, y dale al lado GPIO un camino sólido y de baja impedancia de vuelta al pin de tierra de la Pi. Una tierra flotante o compartida es una forma muy efectiva de convertir el ruido de conmutación de un motor en flancos espurios en una entrada lógica.
Sospechoso 2: rebote del interruptor
Incluso con un pull-up sólido y una tierra limpia, una sola pulsación física seguía disparando el callback dos o tres veces. Eso es rebote mecánico normal: los contactos metálicos del interruptor no cierran limpio, vibran unos milisegundos antes de asentarse. bouncetime=50 en add_event_detect es un antirrebote solo por software: ignora más *interrupciones de flanco* durante 50ms después de la primera, pero no filtra la señal en sí, así que un interruptor particularmente ruidoso puede colar eventos igual.
El trazo de abajo es de una captura real durante esta sesión: el panel de arriba es la lectura cruda antes de cualquiera de estas correcciones, el de abajo es después del pull-up y un antirrebote combinado de hardware y software.
La solución que funcionó
Dos cambios cerraron la brecha. Primero, un condensador cerámico de 100nF en paralelo con el botón (del pin GPIO a tierra) forma un filtro pasa-bajos RC simple junto con la resistencia pull-up, que suaviza el rebote rápido en hardware antes de que llegue al pin. Segundo, el antirrebote por software pasó de "ignorar más interrupciones" a "esperar y volver a comprobar el estado del pin":
import RPi.GPIO as GPIO
import time
PIN = 17
DEBOUNCE_S = 0.05
GPIO.setmode(GPIO.BCM)
GPIO.setup(PIN, GPIO.IN, pull_up_down=GPIO.PUD_UP)
def button_callback(channel):
time.sleep(DEBOUNCE_S)
if GPIO.input(channel) == GPIO.LOW:
print("confirmed press")
GPIO.add_event_detect(PIN, GPIO.FALLING, callback=button_callback, bouncetime=200)
try:
while True:
time.sleep(1)
except KeyboardInterrupt:
GPIO.cleanup()
La segunda comprobación después del sleep importa: descarta el evento si el pin volvió a HIGH mientras tanto, en vez de confiar ciegamente en el primer flanco. Combinado con el filtro RC, las diez pulsaciones que antes disparaban de forma confiable entre 3 y 7 callbacks ahora producen exactamente uno.
Lista de chequeo para "funciona en la breadboard, no en la caja"
- Todo pin
GPIO.INtiene unpull_up_downexplícito, incluso los que "por casualidad" leían bien sin él. - Los tramos de cable largos llevan una resistencia externa, no solo el pull interno débil de la Pi.
- El cable de tierra de la entrada no comparte retorno con nada que conmute corriente real (LEDs, relés, motores).
- Un filtro RC en hardware (resistencia + ~100nF) se encarga del rebote rápido; el antirrebote por software hace el resto, y vuelve a comprobar el pin en vez de confiar en la primera interrupción.
- Registrá las lecturas GPIO reales con marca de tiempo antes de cambiar nada: "anda a los tumbos" no es un diagnóstico, un trazo como el de arriba sí lo es.
