01 · Punto de partida
Del dedo al bit
Con el tema 15 sabés escribir pines y con el tema 16 sabés cómo es un pin por dentro, incluidos los umbrales de entrada y el pull-up. Ahora usamos la otra mitad del GPIO: leer. Una entrada digital convierte una tensión en un bit que el programa puede consultar, y casi todo dispositivo interactivo empieza con una: pulsadores, interruptores, finales de carrera, sensores digitales.
En el curso de circuitos secuenciales construiste un antirrebote con un latch SR y viste cómo un contacto mecánico no cambia limpiamente. Aquí se presenta el mismo problema y se resuelve con software, que cuesta menos que un componente y se ajusta con una línea de código.
Retomamos los estados del pin (tema 15), los umbrales y los pull-ups (tema 16) y la base de tiempo con temporizador (tema 11).
02 · Cableado y lectura
Conectar y leer un pulsador
Hay tres formas habituales de conectar un pulsador a una entrada:
| Conexión | Pulsador abierto | Pulsador cerrado | Comentario |
|---|---|---|---|
| Pulsador a masa con pull-up interno | 1 (por el pull-up) | 0 | La más común: no necesita componentes externos. Lógica activa en bajo |
| Pulsador a masa con pull-up externo | 1 | 0 | Igual, con un nivel más firme frente al ruido (10 kΩ) |
| Pulsador a VCC con pull-down externo | 0 (por el pull-down) | 1 | Lógica activa en alto; exige una resistencia externa |
Casi siempre se usa la primera. Se configura el pin como entrada con el pull-up activado y se lee PINx:
DDRD &= ~(1 << PD2); // PD2 como entrada
PORTD |= (1 << PD2); // con el pull-up interno
#define BOTON_PRESIONADO() (!(PIND & (1 << PD2))) // activo en bajo
if (BOTON_PRESIONADO()) {
PORTB |= (1 << PB5); // encender el LED mientras el botón esté apretado
} else {
PORTB &= ~(1 << PB5);
}
La macro con nombre vale la pena: la lógica invertida (0 = presionado) confunde, y esconderla en un solo lugar evita errores.
Un detalle sutil: la entrada pasa por un sincronizador que retrasa la lectura entre medio y un ciclo y medio de reloj. Si leés PINx inmediatamente después de activar el pull-up, puedes obtener el valor viejo; se evita insertando una instrucción nop (__asm__ volatile ("nop");) entre la escritura y la lectura.
Y una precaución eléctrica: una resistencia de 1 kΩ en serie entre el pulsador y el pin protege al microcontrolador si por un error el pin queda configurado como salida mientras alguien aprieta el botón (cortocircuito del pin contra masa).
03 · Niveles y eventos
Sondeo y detección de flancos
Leer el pin una y otra vez dentro del bucle principal se llama sondeo (polling). Con él se distinguen dos conceptos que conviene no confundir:
- El nivel: «¿el botón está apretado ahora?». Sirve para acciones que duran lo que dura la pulsación (mover un motor mientras se aprieta).
- El evento o flanco: «¿acaba de apretarse?». Sirve para acciones puntuales (alternar un LED, incrementar un contador). Se detecta comparando la lectura actual con la anterior.
uint8_t previo = 1; // 1 = suelto
while (1) {
uint8_t ahora = (PIND >> PD2) & 1; // leer una vez por vuelta
if (previo && !ahora) { // 1 -> 0: flanco descendente = se presionó
PORTB ^= (1 << PB5); // alternar el LED una sola vez
}
previo = ahora; // recordar para la próxima vuelta
}
Sin la variable previo, el programa alternaría el LED en cada vuelta del bucle mientras el botón siga apretado: millones de veces por segundo. Es una máquina de estados mínima, como las del curso anterior: el estado es «cómo estaba el botón la vez pasada».
Este programa funciona perfectamente con un botón ideal. Con uno real, el LED se alterna a veces una vez y a veces tres. Es el rebote.
04 · Física del contacto
El rebote de los contactos
Los contactos de un pulsador son láminas metálicas que se golpean y rebotan antes de asentarse. Durante ese tiempo el circuito se abre y se cierra decenas de veces, a una velocidad mucho mayor que la del programa.
Su duración varía con el pulsador, su antigüedad y la fuerza de quien lo aprieta. Un orden de magnitud: de 1 a 10 ms en un pulsador común (los buenos, menos de 1 ms; los malos o gastados, 20 ms). Una microinterruptor tipo «tact switch» de placa suele estar entre 1 y 5 ms.
Un bucle de sondeo que tarda unos pocos microsegundos ve cada rebote. Un solo toque se convierte en N flancos descendentes, y el programa cuenta N pulsaciones. El efecto es aleatorio, y por eso desconcierta: a veces anda y a veces no.
El rebote también ocurre al soltar. Un filtro debe cubrir los dos sentidos.
05 · Con componentes
Antirrebote por hardware
Se puede resolver el rebote antes de que llegue al microcontrolador. Las dos técnicas clásicas:
- Filtro RC con entrada Schmitt. Un capacitor en paralelo con el pulsador suaviza los saltos. La constante de tiempo debe ser mayor que el rebote: con 10 kΩ y 1 µF, τ = 10 ms. El disparador de Schmitt de la entrada (tema 16) evita que la rampa lenta provoque lecturas erráticas.
- Latch SR con un pulsador de dos posiciones (inversor), el circuito del curso de circuitos secuenciales. El primer contacto fija el latch y los rebotes siguientes no lo cambian. Es la solución más limpia, pero necesita un pulsador conmutador y un integrado.
Las dos cuestan componentes, espacio y dinero. En un producto con muchos botones, multiplicar ese costo es caro. Por eso casi siempre se resuelve por software, que es gratis.
06 · La solución ingenua
Antirrebote por retardo
La primera idea de quien programa: al detectar la pulsación, esperar a que pase el rebote y volver a mirar.
if (BOTON_PRESIONADO()) {
_delay_ms(20); // esperar a que pase el rebote
if (BOTON_PRESIONADO()) { // ¿sigue apretado? entonces es real
PORTB ^= (1 << PB5);
while (BOTON_PRESIONADO()) { } // esperar a que se suelte
_delay_ms(20); // y a que pase el rebote del soltado
}
}
Funciona con un botón y un programa que no hace otra cosa. Su costo es la espera bloqueante: durante 20 ms (más todo el tiempo que el botón esté apretado) el microcontrolador no hace nada más. Son 320 000 ciclos a 16 MHz. En 20 ms llegan 230 bytes por una UART a 115 200 baudios, y se pierden. No sirve para un programa que atiende varias cosas a la vez.
El problema de fondo: tratar el tiempo como algo en lo que se «espera», en lugar de algo que se «mide» mientras se hace otra cosa. La solución es la siguiente.
07 · La solución robusta
Antirrebote por muestreo periódico
La idea: no esperar. Una base de tiempo (el temporizador del tema 11) marca cada pocos milisegundos; en cada marca se lee el pin una vez y se actualiza un pequeño estado. La decisión no depende de una espera, sino de varias lecturas seguidas. El resto del tiempo el programa hace otras cosas. Hay tres variantes.
N lecturas iguales seguidas
Un cambio se acepta solo si el pin muestra el nuevo nivel en N muestras consecutivas. Un rebote rápido nunca consigue N lecturas iguales, y un cambio real sí.
Contador integrador
Un contador sube si el pin está en 0 y baja si está en 1, saturado entre 0 y N. Se considera presionado al llegar a N y suelto al llegar a 0. Es más tolerante a lecturas aisladas erróneas, como un pico de ruido.
// Se llama cada 1 ms desde la base de tiempo
if (!(PIND & (1 << PD2))) { if (contador < N) contador++; }
else { if (contador > 0) contador--; }
if (contador == N) presionado = 1;
else if (contador == 0) presionado = 0;
Historial de bits
Cada muestra entra por la derecha de un registro de desplazamiento de 16 bits. La pulsación se reconoce mirando solo las lecturas más recientes (siempre 0) y las más antiguas (siempre 1), e ignorando las del medio, donde ocurre el rebote. Es el mismo registro de desplazamiento que estudiaste en circuitos secuenciales, hecho con una variable:
// Cada 1 ms:
historial = (historial << 1) | ((PIND >> PD2) & 1); // uint16_t, la muestra nueva en el bit 0
if (!presionado && (historial & 0xC007) == 0xC000) { // 3 muestras nuevas en 0, 2 viejas en 1
presionado = 1;
evento_pulsacion(); // una sola vez por pulsación
} else if (presionado && (historial & 0xC007) == 0x0007) {
presionado = 0; // se soltó
}
Con 16 muestras a 1 ms, la ventana ignorada es de 11 ms, más larga que el rebote típico. La pulsación se declara 3 ms después de que se estabilice, y el latencia total queda por debajo de los 20 ms, imperceptibles para una persona (el umbral de percepción está en las decenas de milisegundos).
08 · Un puerto, varios botones
Varios pulsadores en un puerto
Si los botones están en el mismo puerto, se leen todos de una vez con una sola lectura de PINx, y los flancos de cada uno se obtienen con operaciones de bits sobre el byte entero:
static uint8_t previo = 0xFF; // con pull-up: 1 = suelto, para los ocho
uint8_t actual = PIND; // un solo acceso al puerto
uint8_t cambios = actual ^ previo; // 1 en los bits que cambiaron
uint8_t presionados = cambios & ~actual; // cambió y ahora vale 0: se presionó
uint8_t soltados = cambios & actual; // cambió y ahora vale 1: se soltó
previo = actual;
if (presionados & (1 << PD2)) { /* el botón de PD2 acaba de apretarse */ }
Es una forma muy eficiente: los ocho botones se procesan con cuatro instrucciones. Para filtrar los rebotes de todos a la vez existe la técnica de los contadores verticales, que mantiene el contador de cada botón repartido en los bits de varios bytes y actualiza los ocho con operaciones lógicas. Para pocos botones basta con aplicar el filtro de historial a cada uno.
Cuando hay muchos botones (un teclado), se agrupan en una matriz de filas y columnas, que ahorra pines. Es el tema del taller siguiente.
09 · Analogía con programación
Contar pulsaciones con y sin filtro
Este programa simula miles de pulsaciones con rebote aleatorio, muestreadas cada 1 ms, y compara un programa sin filtro (cuenta cada flanco descendente) con el filtro de historial de 8 y de 16 bits. Para el filtro mide qué porcentaje de las pulsaciones se cuentan exactamente una vez.
Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.
import random
random.seed(3)
def muestras(rebote_ms, total_ms=40, t_pulsa=5):
"""Nivel del pin cada 1 ms (1 = suelto, 0 = presionado): durante el rebote, al azar."""
niveles = []
for t in range(total_ms):
if t < t_pulsa:
niveles.append(1)
elif t < t_pulsa + rebote_ms:
niveles.append(random.choice((0, 1)))
else:
niveles.append(0)
return niveles
def contar_ingenuo(niveles):
"""Cuenta cada flanco descendente: lo que haría un programa sin filtro."""
return sum(1 for a, b in zip(niveles, niveles[1:]) if a == 1 and b == 0)
def contar_con_historial(niveles, ancho=8, nuevas=3, viejas=2):
"""Historial de `ancho` bits (la muestra más nueva en el bit 0). Pulsación: las `nuevas`
últimas muestras valen 0 y las `viejas` más antiguas valen 1; las del medio se ignoran."""
mascara = ((1 << nuevas) - 1) | (((1 << viejas) - 1) << (ancho - viejas))
patron_pulsa = ((1 << viejas) - 1) << (ancho - viejas)
patron_suelta = (1 << nuevas) - 1
historial, presionado, pulsaciones = (1 << ancho) - 1, False, 0
for nivel in niveles:
historial = ((historial << 1) | nivel) & ((1 << ancho) - 1)
if not presionado and historial & mascara == patron_pulsa:
presionado = True
pulsaciones += 1
elif presionado and historial & mascara == patron_suelta:
presionado = False
return pulsaciones
print("Una pulsación real, 2000 pruebas por fila, muestreo cada 1 ms")
print("rebote sin filtro historial de 8 bits historial de 16 bits (pulsaciones contadas = 1)")
for rebote in (0, 2, 4, 6, 8, 10):
pruebas = [muestras(rebote) for _ in range(2000)]
promedio = sum(contar_ingenuo(p) for p in pruebas) / len(pruebas)
bien8 = sum(1 for p in pruebas if contar_con_historial(p, 8) == 1) / len(pruebas) * 100
bien16 = sum(1 for p in pruebas if contar_con_historial(p, 16) == 1) / len(pruebas) * 100
print(f"{rebote:>3} ms {promedio:>5.2f} cuentas {bien8:5.1f} % correcto {bien16:5.1f} % correcto")
Resultado: sin filtro, 4 ms de rebote producen en promedio 1,7 pulsaciones por toque, y 10 ms producen 3,3. El historial de 8 bits funciona bien hasta unos 2 ms y falla a medida que el rebote supera su ventana (78 % con 10 ms). El de 16 bits, con una ventana de 11 muestras ignoradas, acierta el 100 % hasta 10 ms. Probá ancho=16, nuevas=4, viejas=3 o rebotes de 14 ms.
El modelo de rebote (cada milisegundo cae al azar en 0 o 1) es más hostil que el real, donde los saltos son más ordenados. Sirve para ver la tendencia: la ventana del filtro debe ser más larga que el rebote.
10 · Exploración interactiva
Simulador de rebote
El simulador genera una pulsación con rebote aleatorio (el nivel del pin, con pull-up: 1 suelto, 0 apretado) y aplica el método de lectura que elijas. Mostrará cuántas pulsaciones y soltados cuenta el programa. Una lectura correcta cuenta exactamente una y una.
Pulsador con rebote
La pulsación real ocurre a los 10 ms y se suelta a los 150 ms. El método «sin filtro» sondea cada 0,02 ms, como un bucle rápido de C; los otros muestrean con el período indicado (el retardo bloqueante usa sondeo rápido y espera 20 ms). El rebote es aleatorio pero reproducible: el botón «Otro rebote aleatorio» cambia la muestra. Es un modelo, no una medición de un pulsador real.
Recorrido guiado
- Con «Sin filtro» y 5 ms, contá cuántas pulsaciones registra el programa para un solo toque. Cambiá el rebote a 0: cuenta una. Subí a 12 ms y mirá cómo crece.
- Cambiá a «Retardo bloqueante»: siempre cuenta una, pero con 20 ms de latencia en los que el programa está parado.
- Elegí «N lecturas iguales» con período de 1 ms y N = 4: funciona sin fallos hasta unos 8 ms. Con 12 ms falla de vez en cuando y con 15 ms en más del 10 % de las muestras. Probá «Otro rebote aleatorio» varias veces para verlo.
- Con 12 ms de rebote, subí N a 6 y el período a 3 ms (18 ms): ahora cuenta bien, a costa de una latencia de unos 18 ms.
- Probá el «Contador integrador»: se comporta de forma parecida, pero tolera mejor una lectura aislada errónea. Compará las latencias de cada método.
Desafío: encontrar el mínimo N que funciona
Fijá el rebote en 15 ms y el período en 1 ms con «N lecturas iguales». Con N = 4 falla en una de cada seis o siete muestras. Probá N = 5, 6, 7 y 8 con «Otro rebote aleatorio» y anotá desde cuándo deja de fallar: en este modelo, donde el nivel cambia más rápido que el muestreo, alcanzan unas 8 lecturas. La regla segura para el peor caso (un contacto que pueda quedar abierto o cerrado varios milisegundos seguidos) es que N × período supere el rebote: 15 ms pide, por ejemplo, un período de 3 ms con N = 6. La latencia resultante es el precio de un pulsador tan malo; a veces conviene cambiarlo.
11 · Método de trabajo
Cómo diseñar la lectura de un botón
- Elegí la conexión: pulsador a masa con pull-up interno, salvo que haya ruido o cables largos.
- Decidí qué necesitás: ¿el nivel (mientras esté apretado) o el evento (cuando se aprieta)?
- Medí el rebote de tu pulsador real con un osciloscopio o un analizador lógico (tema 14); no supongas.
- Elegí el filtro: retardo si el programa es trivial; muestreo periódico con una base de tiempo en cualquier otro caso.
- Dimensioná la ventana: período × N mayor que el rebote medido, con margen, y latencia total menor que unas decenas de milisegundos.
- Probá con pulsaciones rápidas, lentas y con el botón mantenido, y contá los eventos.
Si los botones se leen en una interrupción en vez de por sondeo (tema 20), el problema del rebote sigue ahí: cada rebote disparará una interrupción. El antirrebote se hace igual, con una base de tiempo o desactivando la interrupción durante la ventana de rebote.
12 · Comprensión
Confusiones frecuentes
«Mi pulsador es bueno, no rebota»
Todo contacto mecánico rebota; solo cambia cuánto. Algunos duran decenas de microsegundos, pero un bucle rápido los ve igual. Se mide, no se supone.
«Un retardo de 50 ms por las dudas lo resuelve»
Lo resuelve a costa de paralizar el programa y, si es demasiado largo, de perder pulsaciones rápidas o dar sensación de lentitud. Además, el rebote del soltado también requiere un tratamiento.
«Leer el nivel alcanza para contar pulsaciones»
Contar exige detectar el flanco (la transición), recordando la lectura anterior. Con solo el nivel, se cuenta una vez por vuelta del bucle mientras el botón esté apretado.
«Con una interrupción no hay rebote»
Cada rebote es un flanco y dispara una interrupción. La interrupción acelera la reacción, pero no resuelve el rebote.
«El antirrebote por software hace menos confiable al botón»
Hace lo contrario: elimina las pulsaciones fantasma. Su único costo es una latencia de unos milisegundos, imperceptible, que se puede ajustar.
13 · Práctica guiada
Predecí antes de comprobar
1. Leer un puerto
Seis botones están en PD2 a PD7 con pull-up, y se lee PIND = 0b11111011. ¿Cuál está apretado?
Ver solución
Con lógica activa en bajo, el bit en 0 es el botón apretado. Solo el bit 2 vale 0: el botón de PD2.
2. Flancos de varios botones
previo = 0b11111111 y actual = 0b11110101. ¿Qué botones se acaban de presionar?
Ver solución
cambios = previo ^ actual = 00001010. presionados = cambios & ~actual = 00001010 & 00001010 = 00001010. Se presionaron los botones de PD1 y PD3.
3. Dimensionar el filtro
Un pulsador rebota hasta 8 ms. Muestreás cada 2 ms con el filtro de N lecturas iguales. ¿Qué N elegís y cuál es la latencia?
Ver solución
N × 2 ms debe superar 8 ms: N = 5 da 10 ms con algo de margen. La pulsación se confirma después de unas 5 muestras, es decir, unos 10 ms de latencia: imperceptible.
4. El costo del retardo
Un programa espera 20 ms por cada rebote con _delay_ms(20) mientras recibe bytes a 115 200 baudios (10 bits por byte). ¿Cuántos bytes llegan durante la espera?
Ver solución
115 200 / 10 = 11 520 bytes por segundo. En 20 ms: 11 520 × 0,02 ≈ 230 bytes. Con una UART de un byte de buffer se pierden casi todos.
5. Qué hace el programa sin previo
Un programa alterna un LED con if (BOTON_PRESIONADO()) PORTB ^= (1 << PB5); dentro del bucle principal, sin variable previo. ¿Qué se ve al mantener apretado el botón?
Ver solución razonada
El LED se invierte en cada vuelta del bucle (cientos de miles de veces por segundo) mientras el botón esté apretado. La vista solo percibe un brillo tenue, y al soltar queda encendido o apagado al azar. Falta detectar el flanco: actuar una sola vez al pasar de suelto a apretado.
14 · Síntesis
Ideas para recordar
- Un pulsador a masa con el pull-up interno se lee en lógica activa en bajo: 0 es apretado.
- Para actuar una vez por pulsación se detecta el flanco comparando la lectura actual con la anterior.
- Todo contacto mecánico rebota entre 1 y 20 ms, al apretar y al soltar, y un bucle rápido ve cada rebote.
- El rebote se puede filtrar por hardware (RC con Schmitt, latch SR) o por software.
- El retardo bloqueante es simple pero paraliza el programa; el muestreo periódico no espera.
- Las variantes del muestreo son N lecturas iguales, contador integrador e historial de bits con máscara.
- El período de muestreo por N debe superar el rebote medido; con un puerto entero, los flancos de todos los botones salen con operaciones de bits.
¿Listo para el tema 18?
Antes de avanzar, explicá la diferencia entre nivel y flanco, dimensioná un filtro para un rebote de 6 ms y calculá los eventos de un puerto con previo y actual. Si podés justificar las tres respuestas, sabés leer entradas digitales sin que el mundo mecánico te engañe.
En el próximo tema aplicamos todo en un taller de LED, pulsadores y teclado matricial: un teclado de 16 teclas con solo 8 pines mediante el barrido de filas y columnas.