← Índice del curso

Sensores y Actuadores · Tema 8

Taller: panel de entradas digitales con eventos

Reunimos lo aprendido sobre contactos, rebotes y umbrales en un solo componente reutilizable: un detector de eventos que convierte cualquier entrada digital en clics, dobles clics, pulsaciones largas y repeticiones, sin bloquear el programa y con la polaridad resuelta en un único lugar.

01 · Punto de partida

Qué vamos a construir

En los temas 6 y 7 resolvimos los problemas uno por uno: la conexión con pull-up, el rebote, la detección de flancos, la histéresis. Si cada botón de un proyecto repite ese código a mano, el programa se llena de variables sueltas y copias casi idénticas, y cualquier cambio se multiplica por el número de entradas. Este taller consolida todo en un componente reutilizable.

El proyecto es el panel de control de un pequeño equipo con cuatro entradas:

  • MENÚ (pulsador): clic para avanzar, doble clic para volver, pulsación larga para entrar a configuración.
  • SUBIR y BAJAR (pulsadores): un clic cambia el valor en una unidad, y al mantenerlos aprieta en repetición automática.
  • PUERTA (final de carrera normalmente cerrado): informa cuándo se abre y se cierra, y dispara una alarma si queda abierta más de 3 segundos.

La meta no es el panel en sí, sino la estructura: una sola función, actualizar(), que recibe una entrada, la filtra, detecta los eventos y devuelve qué pasó. El programa principal solo reacciona a eventos y no sabe nada de tiempos, rebotes ni polaridad.

02 · Paso 1

Especificar eventos y tiempos

Antes de escribir código, se definen los eventos y los tiempos como constantes con nombre. Son decisiones de interfaz, no de implementación, y conviene que se puedan cambiar en un solo lugar:

Eventos del detector y cuándo se emiten.
EventoSe emite cuando…Tiempo asociado
EV_PRESIONADOEl contacto pasa a «activo» y el filtro lo confirmaRebote: 20 ms
EV_LIBERADOEl contacto vuelve a «inactivo» y el filtro lo confirmaRebote: 20 ms
EV_CLICKUna pulsación corta termina (sin doble clic ni pulsación larga)Inmediato al soltar; con doble clic habilitado, tras la ventana de 300 ms
EV_DOBLEUna segunda pulsación comienza poco después de soltar la primeraVentana: 300 ms
EV_LARGALa entrada lleva activa más que el umbralPor entrada: 800 ms (botones), 3000 ms (puerta)
EV_REPETICIONTras la pulsación larga, cada intervalo mientras siga activaIntervalo: 150 ms

Los eventos se devuelven como bits de una máscara: una sola llamada puede producir varios a la vez (por ejemplo, EV_PRESIONADO y EV_DOBLE en la misma pulsación). Es más simple que una lista y no usa memoria dinámica.

Una decisión de diseño relevante: la pulsación corta (EV_CLICK) solo se puede emitir al soltar, porque hasta entonces no se sabe si será larga. Y si una entrada admite doble clic, el clic simple tiene que esperar a que venza la ventana para saber que no viene un segundo. Por eso el doble clic es una opción por entrada: se activa solo en el MENÚ, donde vale la pena el retraso de 300 ms.

03 · Paso 2

El cableado

Los tres pulsadores van del pin a masa, con el pull-up interno activado. El final de carrera de la puerta es un contacto normalmente cerrado que la puerta mantiene cerrado cuando está cerrada; también va del pin a masa con pull-up. La relación entre estado físico y nivel eléctrico queda así:

Conexión y niveles del panel (Arduino Uno, pull-up interno en todos los pines).
EntradaPinContactoNivel en reposoNivel «activo»
MENÚD2NO a masaAlto (suelto)Bajo (pulsado)
SUBIRD3NO a masaAltoBajo
BAJARD4NO a masaAltoBajo
PUERTAD5NC a masaBajo (puerta cerrada: contacto cerrado)Alto (puerta abierta o cable cortado)

Observá la particularidad de PUERTA: su estado de alarma es el nivel alto, la inversa de los pulsadores. El detector lo maneja con un parámetro activoBajo. Así, el resto del programa habla siempre de «activo» e «inactivo», y el hecho de que el nivel eléctrico sea el opuesto queda resuelto en la declaración. Además, esta elección es la de seguridad por falla del tema 6: un cable cortado se parece a una puerta abierta y dispara la alarma.

Para los cables largos hasta la puerta, convienen una resistencia en serie de 1 kΩ junto al pin, un capacitor de 100 nF y un pull-up externo de 4,7 kΩ, como se vio en el tema 6.

04 · Paso 3

Datos y antirrebote

Cada entrada tiene su propia configuración y su propio estado interno. Se agrupan en una estructura, para que el detector sea una función que recibe una referencia a ella. Los miembros de configuración se fijan al iniciar; los de estado arrancan en cero.

// Tiempos (ms)
const uint16_t T_REBOTE = 20;
const uint16_t T_DOBLE  = 300;
const uint16_t T_REP    = 150;

// Eventos: bits de una máscara
const uint8_t EV_PRESIONADO = 1 << 0;
const uint8_t EV_LIBERADO   = 1 << 1;
const uint8_t EV_CLICK      = 1 << 2;
const uint8_t EV_DOBLE      = 1 << 3;
const uint8_t EV_LARGA      = 1 << 4;
const uint8_t EV_REPETICION = 1 << 5;

struct Entrada {
    // configuración
    uint8_t  pin;
    bool     activoBajo;      // true: «activo» = nivel bajo (pull-up)
    bool     dobleClic;       // ¿distingue doble clic?
    bool     repite;          // ¿repite al mantener?
    uint16_t tLarga;          // ms hasta la pulsación larga
    // estado interno (arranca en cero)
    bool     estable, candidato, larga, esperando, suprimirClic;
    uint32_t tCambio, tPresion, tLiberacion, tRep;
};

void iniciar(Entrada &e, uint8_t pin, bool activoBajo, bool dobleClic,
             bool repite, uint16_t tLarga) {
    memset(&e, 0, sizeof e);
    e.pin = pin;
    e.activoBajo = activoBajo;
    e.dobleClic = dobleClic;
    e.repite = repite;
    e.tLarga = tLarga;
    pinMode(pin, INPUT_PULLUP);
}

// Traduce el nivel eléctrico a «activo / inactivo». La polaridad vive solo aquí.
bool leerCrudo(const Entrada &e) {
    return (digitalRead(e.pin) == LOW) == e.activoBajo;
}

El tamaño de cada Entrada es de 27 bytes: 1 del pin, 3 de opciones, 2 del umbral largo, 5 booleanos de estado y 16 de cuatro marcas de tiempo de 32 bits. Con cuatro entradas son 108 bytes de SRAM, un 5 % de los 2 KB del ATmega328P. leerCrudo aplica el mismo criterio de abstracción del tema 6 y del futuro tema 40: un único punto del código conoce la polaridad.

05 · Paso 4

La máquina de eventos

La función actualizar() hace dos cosas en orden. Primero aplica el antirrebote del tema 6 (aceptar un cambio solo si se mantiene durante T_REBOTE). Después, con el estado ya limpio, traduce los cambios y los tiempos a eventos. Las variables larga, esperando y suprimirClic son las «banderas» de la máquina de estados:

Estados implícitos de una entrada y qué los hace cambiar.
SituaciónBanderasQué ocurre al…
Libretodas en falsopulsar: emite PRESIONADO
Pulsada, aún cortaestable = verdaderosoltar: emite LIBERADO y CLICK (o espera, si hay doble clic); al pasar tLarga: emite LARGA
Pulsación larga en cursolarga = verdaderomantener: emite REPETICION cada T_REP; al soltar: solo LIBERADO
Esperando segundo clicesperando = verdaderopulsar a tiempo: emite DOBLE; si vence la ventana: emite CLICK
Segunda pulsación (tras el doble clic)suprimirClic = verdaderosoltar: emite LIBERADO sin CLICK
uint8_t actualizar(Entrada &e) {
    uint32_t ahora = millis();
    uint8_t ev = 0;

    // 1) Antirrebote: un cambio se acepta si dura T_REBOTE
    bool crudo = leerCrudo(e);
    if (crudo != e.candidato) {
        e.candidato = crudo;
        e.tCambio = ahora;
    }
    bool cambio = (e.candidato != e.estable) && (ahora - e.tCambio >= T_REBOTE);
    if (cambio) e.estable = e.candidato;

    // 2) Eventos
    if (cambio && e.estable) {                        // se pulsó
        ev |= EV_PRESIONADO;
        e.tPresion = ahora;
        e.larga = false;
        if (e.esperando && ahora - e.tLiberacion <= T_DOBLE) {
            ev |= EV_DOBLE;                            // segundo clic a tiempo
            e.esperando = false;
            e.suprimirClic = true;
        }
    } else if (cambio) {                               // se soltó
        ev |= EV_LIBERADO;
        if (e.suprimirClic) {
            e.suprimirClic = false;
        } else if (!e.larga) {
            if (e.dobleClic) {                         // esperar por si viene otro
                e.esperando = true;
                e.tLiberacion = ahora;
            } else {
                ev |= EV_CLICK;
            }
        }
    } else if (e.estable) {                            // sigue pulsada
        if (!e.larga && ahora - e.tPresion >= e.tLarga) {
            e.larga = true;
            e.tRep = ahora;
            ev |= EV_LARGA;
        } else if (e.larga && e.repite && ahora - e.tRep >= T_REP) {
            e.tRep = ahora;
            ev |= EV_REPETICION;
        }
    }

    // 3) La ventana de doble clic venció: era un clic simple
    if (e.esperando && !e.estable && ahora - e.tLiberacion > T_DOBLE) {
        e.esperando = false;
        ev |= EV_CLICK;
    }
    return ev;
}

Tres propiedades merecen atención. Primero, no bloquea: nunca espera, solo compara millis() con marcas guardadas. Segundo, toda la aritmética de tiempos usa restas sin signo (ahora - marca), que siguen siendo correctas cuando millis() se desborda a los 49 días. Tercero, el estado de cada entrada es local a su estructura: se pueden crear tantas como se quiera sin interferencias.

06 · Paso 5

El programa completo

Con el detector, el programa principal queda corto y declarativo: inicializa las cuatro entradas y, en cada vuelta, pregunta por los eventos y reacciona. Fijate que no aparece ningún tiempo ni nivel eléctrico: solo decisiones de interfaz.

Entrada menu, subir, bajar, puerta;
int valor = 50;

void setup() {
    Serial.begin(115200);
    //      entrada  pin  activoBajo  dobleClic  repite  tLarga
    iniciar(menu,    2,   true,       true,      false,  800);
    iniciar(subir,   3,   true,       false,     true,   800);
    iniciar(bajar,   4,   true,       false,     true,   800);
    iniciar(puerta,  5,   false,      false,     false,  3000);  // NC: «activo» = nivel alto
}

void loop() {
    uint8_t e;

    e = actualizar(menu);
    if (e & EV_CLICK) Serial.println(F("MENU: siguiente"));
    if (e & EV_DOBLE) Serial.println(F("MENU: volver"));
    if (e & EV_LARGA) Serial.println(F("MENU: configuración"));

    e = actualizar(subir);
    if (e & (EV_CLICK | EV_LARGA | EV_REPETICION)) {
        valor = min(valor + 1, 100);
        Serial.println(valor);
    }

    e = actualizar(bajar);
    if (e & (EV_CLICK | EV_LARGA | EV_REPETICION)) {
        valor = max(valor - 1, 0);
        Serial.println(valor);
    }

    e = actualizar(puerta);
    if (e & EV_PRESIONADO) Serial.println(F("PUERTA: abierta"));
    if (e & EV_LIBERADO)   Serial.println(F("PUERTA: cerrada"));
    if (e & EV_LARGA)      Serial.println(F("ALARMA: puerta abierta más de 3 s"));

    // ... aquí corre el resto del programa, sin esperas
}

Para SUBIR y BAJAR, EV_CLICK cubre el toque breve, y EV_LARGA y EV_REPETICION el mantenido. Como tras una pulsación larga no se emite EV_CLICK, no hay un incremento adicional al soltar. Ese tipo de detalle es lo que justifica concentrar la lógica en un solo lugar.

Para PUERTA se aprovechan los mismos eventos con otro significado: EV_PRESIONADO es «se abrió», EV_LIBERADO es «se cerró» y EV_LARGA con 3000 ms es «abierta demasiado tiempo». La reutilización viene de que el detector habla en términos de actividad, no de botones.

07 · Paso 6

Verificar

Un detector de eventos se prueba con una tabla de casos, igual que una función cualquiera. Cada fila es una acción física y el evento que debe aparecer en el monitor serie. Las pruebas con rebotes reales requieren un pulsador de verdad; las de tiempos se pueden hacer a mano con un cronómetro o, mejor, comparando con un analizador lógico que registre la entrada.

Plan de pruebas del panel (tiempos medidos desde el instante físico).
AcciónEntradaResultado esperado
Toque breve (60 ms)MENÚ«siguiente» unos 320 ms después de soltar (20 ms de filtro + 300 ms de ventana)
Dos toques con 140 ms de pausaMENÚ«volver» al comenzar el segundo toque (unos 20 ms después); ningún «siguiente»
Mantener 1 sMENÚ«configuración» a los 820 ms; sin «siguiente» al soltar
Contacto de 4 ms (ruido)cualquieraNingún evento
Toque breveSUBIR+1 inmediatamente al soltar
Mantener 1,5 sSUBIR+1 a los 820 ms y +1 cada 150 ms después (5 incrementos en total); ninguno al soltar
Abrir la puertaPUERTA«abierta»; a los 3 s, «ALARMA»
Cerrar la puertaPUERTA«cerrada»
Desconectar el cablePUERTAIgual que abrir la puerta
Pulsar varias a la vezMENÚ, SUBIR, BAJARCada una produce sus eventos de forma independiente

Si algo no coincide, conviene aislar la capa: primero mostrar por serie el valor de leerCrudo() (¿la polaridad es correcta?), luego el de estable (¿el filtro funciona?) y recién después los eventos. Es el método de las etapas del tema 4 aplicado al software.

08 · Paso 7

Fallas y robustez

Un componente reutilizable se vuelve confiable cuando se piensa qué le puede pasar. Las situaciones que conviene considerar:

  • El bucle principal tarda demasiado. El filtro y los tiempos se evalúan cada vez que se llama a actualizar(). Si una vuelta del loop tarda 40 ms (por un delay, una pantalla lenta o un cálculo largo), el antirrebote de 20 ms deja de ser preciso, un toque de 30 ms puede pasar inadvertido y las repeticiones saldrán a destiempo. Regla práctica: el bucle debe iterar en menos de 5 a 10 ms; si no se puede, la lectura se mueve a una interrupción de temporizador.
  • El arranque. Con la puerta abierta al encender, el detector ve nivel activo desde el primer instante y emite EV_PRESIONADO (y la alarma a los 3 s). Es el comportamiento deseado: el panel informa el estado real, no el supuesto.
  • El desborde de millis(). Las restas sin signo lo toleran. Las comparaciones de la forma ahora > marca + T no, y es el error más frecuente al escribir este tipo de código.
  • Entradas pegadas. Un botón que queda siempre pulsado (suciedad, mecanismo roto) genera una pulsación larga y repeticiones continuas. En un equipo real, la repetición debería tener un tope o un tiempo máximo.
  • Contactos que no cambian nunca. Un final de carrera NO cuyo cable se cortó es indistinguible de uno que no se accionó. Para detectarlo, el programa puede exigir que el contacto cambie al menos una vez durante un ciclo de trabajo esperado, o se usa el montaje NC del ejemplo.
  • Eventos que se pierden. El valor devuelto por actualizar() es un resultado de esa llamada: si el programa no lo consulta, el evento desaparece. Si el código que reacciona a los eventos puede tardar, hay que guardar los eventos en una cola o consumirlos de inmediato.

09 · Analogía con programación

La misma máquina, probada con una línea de tiempo

La lógica del detector no depende del hardware, así que se puede probar sin placa: en Python reescribimos actualizar() con el mismo orden de pasos y le aplicamos una línea de tiempo con cinco contactos: un clic, un doble clic, una pulsación larga y un ruido de 4 ms. La función recibe el nivel y el instante de cada milisegundo y devuelve los eventos. Es una prueba de unidad de la máquina de estados, y es exactamente lo que haría una prueba automática del firmware (omitimos la repetición para abreviar).

Python en tu navegador. Los bloques son editables: cambiá los valores y presioná «Ejecutar Python». La primera vez tarda unos segundos en cargar.

T_REBOTE, T_LARGA, T_DOBLE = 20, 800, 300


class Entrada:
    def __init__(self, doble_clic=False):
        self.doble_clic = doble_clic
        self.estable = self.candidato = False
        self.larga = self.esperando = self.suprimir = False
        self.t_cambio = self.t_presion = self.t_liberacion = 0

    def actualizar(self, crudo, ahora):
        eventos = []
        # 1) antirrebote
        if crudo != self.candidato:
            self.candidato, self.t_cambio = crudo, ahora
        cambio = self.candidato != self.estable and ahora - self.t_cambio >= T_REBOTE
        if cambio:
            self.estable = self.candidato
        # 2) eventos
        if cambio and self.estable:
            eventos.append("PRESIONADO")
            self.t_presion, self.larga = ahora, False
            if self.esperando and ahora - self.t_liberacion <= T_DOBLE:
                eventos.append("DOBLE_CLIC")
                self.esperando, self.suprimir = False, True
        elif cambio:
            eventos.append("LIBERADO")
            if self.suprimir:
                self.suprimir = False
            elif not self.larga:
                if self.doble_clic:
                    self.esperando, self.t_liberacion = True, ahora
                else:
                    eventos.append("CLICK")
        elif self.estable and not self.larga and ahora - self.t_presion >= T_LARGA:
            self.larga = True
            eventos.append("LARGA")
        # 3) la ventana de doble clic venció
        if self.esperando and not self.estable and ahora - self.t_liberacion > T_DOBLE:
            self.esperando = False
            eventos.append("CLICK")
        return eventos


# Línea de tiempo del contacto: (instante en que se cierra, instante en que se abre), en ms
contactos = [(100, 160), (500, 560), (700, 760), (1500, 2500), (3000, 3004)]


def crudo(t):
    return any(inicio <= t < fin for inicio, fin in contactos)


for doble in (True, False):
    print(f"--- con doble clic habilitado: {doble} ---")
    entrada = Entrada(doble_clic=doble)
    for t in range(4000):
        for evento in entrada.actualizar(crudo(t), t):
            print(f"{t:5d} ms  {evento}")

Resultado con doble clic habilitado: el primer toque (100–160 ms) se confirma como PRESIONADO a los 120 ms y LIBERADO a los 180 ms, pero el CLICK sale a los 481 ms, al vencer la ventana. El par de toques emite DOBLE_CLIC a los 720 ms y ningún CLICK. La pulsación de 1 s emite LARGA a los 2320 ms (800 ms después de confirmada) y ningún CLICK al soltar. El ruido de 4 ms no produce nada. Con doble clic deshabilitado, cada toque emite CLICK en el mismo instante en que se confirma la liberación (180, 580 y 780 ms), sin espera.

La prueba muestra el costo concreto del doble clic: unos 300 ms de latencia en cada clic simple. Probá cambiar T_DOBLE a 150: el par de toques con 140 ms de pausa todavía cuenta como doble, pero uno con 200 ms ya no. El modelo no incluye rebotes ni repeticiones; sirve para razonar sobre la lógica y los tiempos.

10 · Exploración interactiva

El panel virtual

Este panel ejecuta en tiempo real el mismo detector del taller (con los mismos tiempos: 20 ms de filtro, 300 ms de doble clic, 800 ms de pulsación larga y 150 ms de repetición). Mantené presionados los botones con el mouse, el dedo o la barra espaciadora cuando tienen el foco, y observá los eventos y el valor. Podés agregar rebotes simulados al contacto y cambiar el tiempo del filtro para ver qué pasa cuando no alcanza.

Panel de cuatro entradas

Valor (0 a 100)50
Cambios en las señales crudas0
Eventos de alto nivel0
Estado de la puerta
cerrada

Presioná un botón del panel.

Registro de eventos (los 14 más recientes), con el instante en milisegundos.
InstanteEntradaEvento
Sin eventos todavía.

Los «cambios en las señales crudas» cuentan cuántas veces cambió el nivel que el contacto presentó al programa, incluidos los rebotes. Los «eventos de alto nivel» son clics, dobles clics, pulsaciones largas, repeticiones y los cambios de la puerta. La diferencia entre ambos números es el trabajo del detector.

Recorrido guiado

  1. Con 6 ms de rebote y 20 ms de filtro, tocá MENÚ brevemente: el registro muestra PRESIONADO, LIBERADO y, unos 300 ms después, CLICK. Observá los cambios crudos: son varios, pero el evento es uno solo.
  2. Tocá MENÚ dos veces rápido: aparece DOBLE_CLIC al comenzar el segundo toque y no hay ningún CLICK. Si esperás más de 300 ms entre toques, serán dos CLICK separados.
  3. Mantené MENÚ más de un segundo: LARGA a los 800 ms (contados desde que se confirma la pulsación). Al soltar no hay CLICK.
  4. Tocá SUBIR: el valor sube de 1 en 1, con CLICK inmediato (sin doble clic habilitado). Mantenelo: tras 800 ms comienza a repetir cada 150 ms.
  5. Poné el filtro en 0 ms con rebotes de 12 ms y tocá SUBIR varias veces: cada toque sube el valor más de una unidad. Sin filtro, el rebote se convierte en pulsaciones. Volvé a 20 ms y comprobalo.
  6. Con filtro de 40 ms, intentá un toque muy breve en SUBIR: a veces se pierde. El filtro descarta contactos más cortos que él.
  7. Marcá «Puerta abierta»: aparece PRESIONADO («abierta») y a los 3 s, LARGA («alarma»). Desmarcala: LIBERADO («cerrada»). Ahora marcá «Cable cortado»: el efecto es idéntico. Con el contacto NC, un cable cortado dispara la alarma, que es el comportamiento seguro.
Desafío: tres tiempos para una interfaz agradable

Con el panel, probá reducir el tiempo de doble clic mentalmente: si fuera de 150 ms, ¿se podría hacer doble clic con comodidad? Una persona típica separa dos toques con 100 a 250 ms. Una ventana de 300 ms es un compromiso: cubre a casi todos y agrega 300 ms de espera a los clics simples. Si en tu producto el clic simple debe ser instantáneo, la solución no es acortar la ventana sino no usar doble clic en esa entrada y sustituirlo por una pulsación larga u otro botón.

11 · Método de trabajo

Lo aprendido, para cualquier panel

  1. Pensá en eventos, no en niveles. El resto del programa debe recibir «clic», «larga», «abierta», no el valor de un pin.
  2. Resolvé la polaridad en un único lugar. Un parámetro por entrada y una función de lectura bastan.
  3. Un componente, muchas instancias. Una estructura con su estado y una función que la actualiza: las entradas no se interfieren.
  4. Los tiempos son constantes con nombre, ajustables en un solo sitio, y se justifican por la interfaz y por las mediciones del rebote.
  5. No bloquees nunca. Todo se basa en comparar millis() con marcas guardadas, con restas sin signo.
  6. Mantené el bucle rápido. El detector solo funciona bien si se lo llama a menudo; si el bucle es lento, pasá la lectura a una interrupción periódica.
  7. Probá con una tabla de casos y, siempre que se pueda, con la lógica separada del hardware para ejecutarla en un simulador.
  8. Pensá en la falla. Cable cortado, contacto pegado, arranque con la entrada ya activa: decidí qué quiere el sistema que ocurra en cada caso.

Este patrón (estado local, función de actualización, eventos como resultado) se repite en casi todo el resto del curso: los encoders, los sensores digitales, los motores. El próximo bloque aplica el mismo enfoque a las magnitudes analógicas.

12 · Comprensión

Confusiones frecuentes

«El doble clic es gratis»

Obliga a retrasar cada clic simple por el tiempo de la ventana (300 ms aquí). Es una decisión de usabilidad: solo vale la pena en entradas donde la segunda función sea importante.

«Con una pulsación larga se emite también el clic al soltar»

No en este diseño: una pulsación que alcanzó el tiempo largo ya se consumió como tal. Si no, el usuario obtendría dos acciones con un solo gesto.

«Basta con llamar a actualizar() de vez en cuando»

El filtro y los tiempos solo se evalúan en cada llamada. Si se la llama cada 100 ms, un toque de 60 ms puede no verse, y la repetición no podrá ser más fina que ese período.

«Un cable cortado se detecta solo»

Depende de la polaridad. Con un contacto NC y pull-up, el corte se parece a la apertura; con un NO, se parece al reposo y pasa inadvertido. La elección del tipo de contacto es la protección.

«Los eventos se acumulan hasta que el programa los lee»

No: el valor devuelto es el de esa llamada. Si el código no lo consulta en ese momento, se pierde. Cuando la reacción puede demorar, hay que encolar los eventos.

«Con ahora > marca + T es lo mismo que ahora - marca > T»

Solo hasta que millis() se desborda, a los 49 días en una variable de 32 bits. La resta sin signo sigue funcionando; la suma puede desbordarse antes y producir un resultado absurdo.

13 · Práctica guiada

Predecí antes de comprobar

1. Latencia del clic simple

En el MENÚ (con doble clic habilitado), un usuario suelta el botón en el instante t. ¿Cuánto tarda en emitirse EV_CLICK? ¿Y en SUBIR, que no usa doble clic?

Ver solución

MENÚ: el filtro confirma la liberación a los 20 ms, y la ventana de 300 ms vence a los 301 ms de esa confirmación: unos 320 ms después de soltar. SUBIR: el CLICK se emite al confirmar la liberación, unos 20 ms después de soltar.

2. Contar incrementos

Se mantiene SUBIR pulsado durante 1500 ms (T_REBOTE = 20, T_LARGA = 800, T_REP = 150). El valor inicial es 50. ¿Cuál es el valor final?

Ver solución

La pulsación se confirma a los 20 ms; la pulsación larga ocurre a los 20 + 800 = 820 ms (+1). Las repeticiones salen a los 970, 1120, 1270 y 1420 ms (+4). Al soltar a 1500 ms no hay CLICK. Total de 5 incrementos: el valor final es 55.

3. Cable cortado

En el panel, ¿qué eventos genera un cable cortado en PUERTA? ¿Y si el final de carrera hubiera sido NO con la puerta cerrada como estado accionado?

Ver solución

Con NC y el estado activo en nivel alto: el corte produce EV_PRESIONADO (puerta «abierta») y a los 3 s EV_LARGA (alarma): falla segura. Con un contacto NO, el corte se leería como puerta no accionada: no habría ningún evento, y la falla pasaría inadvertida.

4. Memoria

Cada Entrada ocupa 27 bytes. ¿Cuánta SRAM usan las cuatro y qué porcentaje es de los 2 KB del ATmega328P? ¿Y un teclado de 16 teclas?

Ver solución

4 × 27 = 108 bytes (5,3 % de 2048). Para 16 teclas: 16 × 27 = 432 bytes (21 %). Es considerable: en un teclado conviene una versión reducida del detector, sin los campos de configuración repetidos.

5. Un bucle lento

El loop tarda 40 ms por vuelta (hay una pantalla lenta). ¿Qué problemas aparecen con el detector y cómo se arreglan?

Ver solución

El filtro de 20 ms deja de ser preciso: el estado solo se muestrea cada 40 ms, de modo que los toques de menos de 40 ms pueden perderse, y los tiempos del doble clic, la pulsación larga y la repetición se redondean a múltiplos de 40 ms. Se arregla acelerando el bucle (sin esperas ni dibujos largos) o moviendo la lectura a una interrupción de temporizador cada 1 a 5 ms.

6. Una pulsación muy larga

Hay que agregar un evento «pulsación muy larga» (3 s) en el MENÚ para restablecer valores de fábrica. ¿Qué cambios requiere el detector?

Ver solución

Un nuevo bit de evento (EV_MUY_LARGA), una bandera muyLarga en la estructura (para emitirlo una sola vez) y un parámetro tMuyLarga. En la rama «sigue pulsada»: si larga es verdadero, muyLarga es falso y pasó tMuyLarga desde la presión, se activa la bandera y se emite el evento. La bandera se limpia al pulsar. Ninguna otra parte del programa cambia.

14 · Síntesis

Ideas para recordar

  • Un detector de eventos reutilizable encapsula el antirrebote, los tiempos y la polaridad, y entrega al programa eventos en lugar de niveles.
  • Los eventos son bits de una máscara: una llamada puede emitir varios a la vez.
  • El clic simple solo puede emitirse al soltar, y con doble clic habilitado debe esperar la ventana (aquí 300 ms de latencia).
  • La polaridad se resuelve con un parámetro por entrada en una única función de lectura; el resto habla de «activo» e «inactivo».
  • El detector no bloquea: compara millis() con marcas guardadas, usando restas sin signo que toleran el desborde.
  • Cada entrada guarda su estado en su propia estructura (27 bytes): no hay interferencias entre entradas.
  • El bucle principal debe ser rápido (menos de 5 a 10 ms) o la lectura debe pasar a una interrupción periódica.
  • Un contacto NC convierte el cable cortado en una alarma; la elección del tipo de contacto es parte del diseño de seguridad.
  • La lógica de eventos se prueba con una línea de tiempo y una tabla de casos, aun sin hardware.

¿Listo para el bloque 3?

Antes de avanzar, explicá por qué el clic simple se retrasa cuando una entrada admite doble clic, calculá cuántos incrementos produce un SUBIR mantenido 1,2 s con los tiempos del taller y justificá por qué el final de carrera de la puerta se conecta como normalmente cerrado. Si podés justificar las tres respuestas, tenés el patrón que se repetirá en el resto del curso.

En el próximo bloque pasamos a las magnitudes continuas, empezando por la temperatura: los termistores NTC y PTC, su resistencia y la ecuación de Steinhart-Hart para convertir una lectura en grados.