← Índice del curso

Microcontroladores · Tema 17

Entradas digitales y antirrebote

Leer un pulsador parece lo más simple que puede hacer un microcontrolador, y en el papel lo es: un bit. En la mesa de trabajo, un solo toque del dedo produce decenas de cambios de nivel en pocos milisegundos. Aprender a leer una entrada es, sobre todo, aprender a no dejarse engañar por el mundo mecánico.

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:

Conexiones de un pulsador y su lógica.
ConexiónPulsador abiertoPulsador cerradoComentario
Pulsador a masa con pull-up interno1 (por el pull-up)0La más común: no necesita componentes externos. Lógica activa en bajo
Pulsador a masa con pull-up externo10Igual, con un nivel más firme frente al ruido (10 kΩ)
Pulsador a VCC con pull-down externo0 (por el pull-down)1Ló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.

Rebote de un pulsador El nivel del pin está en alto. Al pulsar, baja, sube y baja varias veces durante unos milisegundos antes de quedar en bajo. Al soltar ocurre algo similar en sentido contrario. 1 (suelto)0 (presionado) rebote: 1 a 10 msrebote al soltar botón apretado, nivel estable en 0

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

Pulsaciones contadas—
Soltados contados—
Latencia hasta la primera detección—

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

  1. 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.
  2. Cambiá a «Retardo bloqueante»: siempre cuenta una, pero con 20 ms de latencia en los que el programa está parado.
  3. 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.
  4. 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.
  5. 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

  1. Elegí la conexión: pulsador a masa con pull-up interno, salvo que haya ruido o cables largos.
  2. Decidí qué necesitás: ¿el nivel (mientras esté apretado) o el evento (cuando se aprieta)?
  3. Medí el rebote de tu pulsador real con un osciloscopio o un analizador lógico (tema 14); no supongas.
  4. Elegí el filtro: retardo si el programa es trivial; muestreo periódico con una base de tiempo en cualquier otro caso.
  5. Dimensioná la ventana: período × N mayor que el rebote medido, con margen, y latencia total menor que unas decenas de milisegundos.
  6. 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.