Un evento de GPIO es una notificación que se dispara cuando cambia el estado de un pin sin tener que preguntarlo constantemente.
En la entrada anterior vimos cómo leer un botón usando el método del Polling (preguntar continuamente). Si bien funciona para ejemplos sencillos, en el mundo real es una técnica a evitar.
Imagina que estás esperando una carta importante.
- Polling es bajar al buzón cada 5 minutos a ver si ha llegado. Te pasas el día subiendo y bajando escaleras, cansado y sin poder hacer otra cosa.
- Interrupciones es instalar un timbre en el buzón. Tú te dedicas a leer un libro o ver una serie, y solo cuando el cartero mete la carta, suena el timbre y bajas.
En nanoFramework, las interrupciones de hardware se exponen mediante eventos de C#.
El uso de eventos nos permite liberar el hilo principal (Main Thread) para que realice tareas complejas (como mantener la conexión Wi-Fi) mientras el hardware vigila los pines por nosotros.
¿Qué es una interrupción de hardware?
A nivel eléctrico, el microcontrolador tiene circuitos específicos conectados a los pines GPIO. Estos circuitos pueden detectar cambios de voltaje (flancos) sin intervención de la CPU.
Cuando el voltaje cambia (por ejemplo, pulsamos un botón y pasa de 3.3V a 0V), este circuito “despierta” a la CPU y le dice: “¡Deja lo que estés haciendo y atiende esto!”.
En nanoFramework, el CLR captura esa señal y la transforma en un evento de C# llamado ValueChanged.
Tipos de disparo (triggers)
Podemos configurar cuándo queremos que salte el evento:
- Rising (Flanco de Subida): Cuando la señal pasa de Low (0) a High (1). (Ej: Soltar un botón con Pull-Up).
- Falling (Flanco de Bajada): Cuando la señal pasa de High (1) a Low (0). (Ej: Pulsar un botón con Pull-Up).
- Both (Ambos): Nos avisa en cualquier cambio.
Suscribirse al evento ValueChanged
Vamos a refactorizar nuestro código anterior. Ya no necesitamos un bucle while que compruebe el botón.
La estructura en C# es la estándar para suscripción de eventos (+=):
// 1. Abrimos el pin (igual que antes)
GpioPin boton = gpio.OpenPin(15, PinMode.InputPullUp);
// 2. Definimos qué cambios queremos escuchar
// Esto es importante para filtrar ruido
boton.SetPinMode(PinMode.InputPullUp);
// 3. Nos suscribimos al evento
boton.ValueChanged += Boton_ValueChanged;Y luego, definimos el método manejador (el Event Handler):
private static void Boton_ValueChanged(object sender, PinValueChangedEventArgs e)
{
// Este código se ejecuta SOLO cuando se pulsa el botón
Debug.WriteLine($"Botón cambiado a: {e.ChangeType}");
}Ejemplo completo: interruptor mediante eventos
Vamos a hacer que el LED cambie de estado cada vez que pulsamos el botón. Fíjate en cómo queda el bucle principal Main.
using System;
using System.Device.Gpio;
using System.Diagnostics;
using System.Threading;
namespace GpioEvents
{
public class Program
{
// Declaramos el LED como estático para acceder desde el evento
private static GpioPin _led;
public static void Main()
{
GpioController gpio = new GpioController();
// Configuración del LED
_led = gpio.OpenPin(2, PinMode.Output);
// Configuración del Botón
GpioPin boton = gpio.OpenPin(15, PinMode.InputPullUp);
// Suscripción al evento
// Al usar '+=' Visual Studio te sugerirá crear el método automáticamente con TAB
boton.ValueChanged += Boton_ValueChanged;
Debug.WriteLine("Sistema listo. Esperando eventos...");
// Bucle principal
// FÍJATE AQUÍ: Ya no hacemos nada.
// Ponemos el hilo a dormir eternamente. El consumo de CPU baja drásticamente.
Thread.Sleep(Timeout.Infinite);
}
private static void Boton_ValueChanged(object sender, PinValueChangedEventArgs e)
{
// Filtramos: Solo queremos actuar cuando se PULSA (Flanco de Bajada / Falling)
// Si no filtramos, el evento saltaría también al soltar el botón
if (e.ChangeType == PinEventTypes.Falling)
{
Debug.WriteLine("¡Botón Pulsado!");
_led.Toggle(); // Invertimos el LED
}
}
}
}Al usar Thread.Sleep(Timeout.Infinite), le indicamos al sistema operativo (RTOS) que este hilo no necesita ciclos de reloj. El procesador puede entrar en modos de bajo consumo o atender a otros hilos (como el de red).
El problema de los rebotes (debouncing)
Si pruebas el código anterior con un botón físico real (no uno de alta calidad), notarás algo raro: a veces, al pulsar una sola vez, el LED se enciende y se apaga instantáneamente. O parpadea.
Esto se llama Rebote (Bouncing).
En el mundo real, un interruptor mecánico es una chapa metálica golpeando otra. Al juntarse, “rebotan” microscópicamente varias veces antes de estabilizarse. El microcontrolador es tan rápido que detecta cada rebote como una pulsación distinta.
Debounce por software
Aunque existen circuitos externos (condensadores) para arreglar esto, en nanoFramework podemos solucionarlo fácilmente por código comprobando el tiempo entre pulsaciones.
Vamos a mejorar nuestro Manejador de Eventos:
// Variable para guardar la última vez que pulsamos
private static DateTime _ultimoRebote = DateTime.MinValue;
private static void Boton_ValueChanged(object sender, PinValueChangedEventArgs e)
{
if (e.ChangeType == PinEventTypes.Falling)
{
// Comprobamos si han pasado al menos 200ms desde la última vez
if ((DateTime.UtcNow - _ultimoRebote).TotalMilliseconds > 200)
{
Debug.WriteLine("Pulsación válida");
_led.Toggle();
// Actualizamos la fecha del último rebote
_ultimoRebote = DateTime.UtcNow;
}
}
}Con este sencillo if, ignoramos cualquier señal espuria que ocurra en los 200ms posteriores a la primera pulsación. ¡Problema resuelto!