Módulo 2

Manejo de interrupciones en ESP32

GPIO y Control Digital

ESP32 Mecatrónica IoT UNAM

Fundamentos de las Interrupciones

Las interrupciones son mecanismos fundamentales en sistemas embebidos que permiten al procesador responder inmediatamente a eventos críticos, pausando temporalmente la ejecución del programa principal para atender tareas prioritarias de tiempo real.

En el ESP32, las interrupciones son especialmente poderosas debido a su arquitectura dual-core, permitiendo un manejo eficiente de múltiples eventos simultáneos sin comprometer el rendimiento del sistema principal.

Interrupciones Externas
  • Activadas por cambios en GPIOs
  • Tipos: RISING, FALLING, CHANGE
  • Prioridad: Alta
  • Latencia: < 1 μs
Interrupciones Internas
  • Generadas por timers
  • Comunicación: I2C, SPI, UART
  • ADC, DAC y watchdog
  • Eventos del sistema
Concepto clave: Las interrupciones permiten sistemas reactivos donde eventos críticos como paradas de emergencia o detección de fallos se procesan instantáneamente.

Tipos y Modos de Activación

El ESP32 soporta múltiples modos de activación de interrupciones externas, cada uno optimizado para diferentes tipos de sensores y eventos:

Modo Descripción Uso Típico Ejemplo Aplicación
RISING Se activa en flanco ascendente (LOW → HIGH) Detección de activación Sensor de proximidad, botón normalmente bajo
FALLING Se activa en flanco descendente (HIGH → LOW) Detección de desactivación Botón con pull-up, parada de emergencia
CHANGE Se activa en cualquier cambio de estado Monitoreo de cambios Encoder rotativo, switch toggle
LOW Se activa mientras el pin esté en LOW Estado continuo bajo Alarma activa, sensor activado
HIGH Se activa mientras el pin esté en HIGH Estado continuo alto Sensor activado, señal de habilitación
Importante: Los modos LOW y HIGH pueden consumir más recursos de CPU si el estado persiste durante largos períodos. Usar con precaución.

Implementación de Interrupciones

La configuración correcta de interrupciones requiere considerar aspectos críticos como la duración de las ISR, el manejo de variables compartidas y la prevención de condiciones de carrera:

C++ - Arduino IDE
// Sistema avanzado de interrupciones múltiples en ESP32
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/queue.h"

// Definición de pines para diferentes tipos de sensores
#define EMERGENCY_BUTTON_PIN    21  // Botón de parada de emergencia
#define DOOR_SENSOR_PIN         22  // Sensor de puerta (reed switch)
#define MOTION_SENSOR_PIN       23  // Sensor de movimiento PIR
#define ENCODER_A_PIN           18  // Encoder rotativo canal A
#define ENCODER_B_PIN           19  // Encoder rotativo canal B

// Pines de salida para actuadores
#define ALARM_PIN               2   // Buzzer de alarma
#define EMERGENCY_LED_PIN       4   // LED de emergencia
#define STATUS_LED_PIN          5   // LED de estado

// Variables volátiles para comunicación con ISRs
volatile bool emergencyTriggered = false;
volatile bool doorOpened = false;
volatile bool motionDetected = false;
volatile long encoderPosition = 0;
volatile unsigned long lastInterruptTime = 0;

// Contador de eventos para estadísticas
volatile unsigned long emergencyCount = 0;
volatile unsigned long doorEvents = 0;
volatile unsigned long motionEvents = 0;
volatile unsigned long encoderEvents = 0;

// Queue para comunicación entre ISR y tarea principal
QueueHandle_t interruptQueue;

// Estructura para eventos de interrupción
struct InterruptEvent {
    uint8_t source;     // Fuente de la interrupción
    uint32_t timestamp; // Momento del evento
    uint8_t state;      // Estado del pin
};

// Códigos de fuente de interrupción
#define INT_EMERGENCY   1
#define INT_DOOR        2
#define INT_MOTION      3
#define INT_ENCODER_A   4
#define INT_ENCODER_B   5

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("\n=== Sistema de Interrupciones Avanzado ESP32 ===");
    
    // Configuración de pines de entrada
    pinMode(EMERGENCY_BUTTON_PIN, INPUT_PULLUP);   // Botón normalmente alto
    pinMode(DOOR_SENSOR_PIN, INPUT_PULLUP);        // Reed switch normalmente cerrado
    pinMode(MOTION_SENSOR_PIN, INPUT);             // PIR sensor
    pinMode(ENCODER_A_PIN, INPUT_PULLUP);          // Encoder canal A
    pinMode(ENCODER_B_PIN, INPUT_PULLUP);          // Encoder canal B
    
    // Configuración de pines de salida
    pinMode(ALARM_PIN, OUTPUT);
    pinMode(EMERGENCY_LED_PIN, OUTPUT);
    pinMode(STATUS_LED_PIN, OUTPUT);
    
    // Estados iniciales
    digitalWrite(ALARM_PIN, LOW);
    digitalWrite(EMERGENCY_LED_PIN, LOW);
    digitalWrite(STATUS_LED_PIN, LOW);
    
    // Crear queue para eventos de interrupción
    interruptQueue = xQueueCreate(20, sizeof(InterruptEvent));
    
    // Configurar interrupciones externas
    attachInterrupt(digitalPinToInterrupt(EMERGENCY_BUTTON_PIN), 
                   emergencyISR, FALLING);
    attachInterrupt(digitalPinToInterrupt(DOOR_SENSOR_PIN), 
                   doorISR, CHANGE);
    attachInterrupt(digitalPinToInterrupt(MOTION_SENSOR_PIN), 
                   motionISR, RISING);
    attachInterrupt(digitalPinToInterrupt(ENCODER_A_PIN), 
                   encoderISR, CHANGE);
    
    Serial.println("Configuración de interrupciones completada:");
    Serial.printf("GPIO %d: Botón emergencia (FALLING)\n", EMERGENCY_BUTTON_PIN);
    Serial.printf("GPIO %d: Sensor puerta (CHANGE)\n", DOOR_SENSOR_PIN);
    Serial.printf("GPIO %d: Sensor movimiento (RISING)\n", MOTION_SENSOR_PIN);
    Serial.printf("GPIO %d: Encoder A (CHANGE)\n", ENCODER_A_PIN);
    Serial.println("\nSistema de seguridad activo...\n");
    
    // LED de estado para indicar sistema funcionando
    digitalWrite(STATUS_LED_PIN, HIGH);
}

void loop() {
    InterruptEvent event;
    
    // Procesar eventos de interrupción desde la queue
    if (xQueueReceive(interruptQueue, &event, pdMS_TO_TICKS(100)) == pdTRUE) {
        processInterruptEvent(event);
    }
    
    // Gestión del sistema de emergencia
    if (emergencyTriggered) {
        handleEmergencyMode();
    }
    
    // Reporte periódico de estadísticas
    static unsigned long lastReport = 0;
    if (millis() - lastReport > 10000) {
        printSystemStatus();
        lastReport = millis();
    }
    
    // Parpadeo del LED de estado
    static unsigned long lastBlink = 0;
    if (millis() - lastBlink > 1000) {
        digitalWrite(STATUS_LED_PIN, !digitalRead(STATUS_LED_PIN));
        lastBlink = millis();
    }
    
    // Pequeña pausa para permitir otras tareas
    delay(10);
}

// ISR para botón de emergencia - Crítica, debe ser rápida
void IRAM_ATTR emergencyISR() {
    unsigned long currentTime = millis();
    
    // Debounce simple
    if (currentTime - lastInterruptTime < 50) {
        return;
    }
    lastInterruptTime = currentTime;
    
    emergencyTriggered = true;
    emergencyCount++;
    
    // Enviar evento a queue
    InterruptEvent event = {INT_EMERGENCY, currentTime, LOW};
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(interruptQueue, &event, &xHigherPriorityTaskWoken);
    
    if (xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}

// ISR para sensor de puerta
void IRAM_ATTR doorISR() {
    unsigned long currentTime = millis();
    uint8_t state = digitalRead(DOOR_SENSOR_PIN);
    
    doorOpened = (state == HIGH);  // HIGH = puerta abierta
    doorEvents++;
    
    InterruptEvent event = {INT_DOOR, currentTime, state};
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(interruptQueue, &event, &xHigherPriorityTaskWoken);
    
    if (xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}

// ISR para sensor de movimiento
void IRAM_ATTR motionISR() {
    unsigned long currentTime = millis();
    
    motionDetected = true;
    motionEvents++;
    
    InterruptEvent event = {INT_MOTION, currentTime, HIGH};
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(interruptQueue, &event, &xHigherPriorityTaskWoken);
    
    if (xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}

// ISR para encoder rotativo
void IRAM_ATTR encoderISR() {
    static uint8_t lastEncoded = 0;
    uint8_t encoded = (digitalRead(ENCODER_A_PIN) << 1) | digitalRead(ENCODER_B_PIN);
    uint8_t sum = (lastEncoded << 2) | encoded;
    
    if (sum == 0b1101 || sum == 0b0100 || sum == 0b0010 || sum == 0b1011) {
        encoderPosition++;
    } else if (sum == 0b1110 || sum == 0b0111 || sum == 0b0001 || sum == 0b1000) {
        encoderPosition--;
    }
    
    lastEncoded = encoded;
    encoderEvents++;
    
    InterruptEvent event = {INT_ENCODER_A, millis(), encoded};
    BaseType_t xHigherPriorityTaskWoken = pdFALSE;
    xQueueSendFromISR(interruptQueue, &event, &xHigherPriorityTaskWoken);
    
    if (xHigherPriorityTaskWoken) {
        portYIELD_FROM_ISR();
    }
}

void processInterruptEvent(InterruptEvent event) {
    switch (event.source) {
        case INT_EMERGENCY:
            Serial.printf("⚠️  EMERGENCIA activada! Timestamp: %lu\n", event.timestamp);
            digitalWrite(EMERGENCY_LED_PIN, HIGH);
            break;
            
        case INT_DOOR:
            Serial.printf("🚪 Puerta %s - Timestamp: %lu\n", 
                         event.state ? "ABIERTA" : "CERRADA", event.timestamp);
            break;
            
        case INT_MOTION:
            Serial.printf("🏃 Movimiento detectado - Timestamp: %lu\n", event.timestamp);
            // Auto-reset del sensor de movimiento después de 5 segundos
            static unsigned long motionTimer = 0;
            motionTimer = event.timestamp;
            break;
            
        case INT_ENCODER_A:
            Serial.printf("🔄 Encoder: posición %ld\n", encoderPosition);
            break;
    }
}

void handleEmergencyMode() {
    // Activar alarma
    digitalWrite(ALARM_PIN, HIGH);
    delay(100);
    digitalWrite(ALARM_PIN, LOW);
    delay(100);
    
    Serial.println("🚨 MODO EMERGENCIA ACTIVO - Todos los procesos detenidos");
    Serial.println("Presione RESET para reiniciar el sistema");
    
    // En una aplicación real, aquí se detendrían motores, se cerrarían válvulas, etc.
    
    // Bloquear hasta reset (en aplicación real, permitir reset por comando autorizado)
    while (emergencyTriggered) {
        digitalWrite(EMERGENCY_LED_PIN, HIGH);
        delay(250);
        digitalWrite(EMERGENCY_LED_PIN, LOW);
        delay(250);
        
        // Verificar si se ha liberado el botón de emergencia
        if (digitalRead(EMERGENCY_BUTTON_PIN) == HIGH) {
            delay(2000);  // Esperar 2 segundos de estabilidad
            if (digitalRead(EMERGENCY_BUTTON_PIN) == HIGH) {
                emergencyTriggered = false;
                digitalWrite(EMERGENCY_LED_PIN, LOW);
                Serial.println("✅ Emergencia desactivada - Sistema restaurado");
            }
        }
    }
}

void printSystemStatus() {
    Serial.println("\n═══ ESTADO DEL SISTEMA ═══");
    Serial.printf("Tiempo activo: %lu segundos\n", millis() / 1000);
    Serial.printf("Estado emergencia: %s\n", emergencyTriggered ? "ACTIVA" : "Normal");
    Serial.printf("Puerta: %s\n", doorOpened ? "ABIERTA" : "CERRADA");
    Serial.printf("Movimiento: %s\n", motionDetected ? "DETECTADO" : "Sin detectar");
    Serial.printf("Posición encoder: %ld\n", encoderPosition);
    Serial.println("─── ESTADÍSTICAS ───");
    Serial.printf("Eventos emergencia: %lu\n", emergencyCount);
    Serial.printf("Eventos puerta: %lu\n", doorEvents);
    Serial.printf("Eventos movimiento: %lu\n", motionEvents);
    Serial.printf("Eventos encoder: %lu\n", encoderEvents);
    Serial.printf("Queue disponible: %d espacios\n", 
                  uxQueueSpacesAvailable(interruptQueue));
    Serial.println("════════════════════════\n");
}

Ejercicios Prácticos

1

Contador de Eventos

Principiante 25 min ISR Básicas

Objetivo: Implementar un contador de pulsos usando interrupciones externas con display en tiempo real.

Características:

  • Contador incremental en flanco ascendente
  • Botón de reset con interrupción separada
  • Display del conteo en Serial Monitor
  • LED que parpadea con cada evento
  • Implementación de debounce en hardware y software

Aplicación: Contadores de piezas, sensor de flujo, RPM

2

Sistema de Alarma Inteligente

Intermedio 40 min Multi-Sensor

Objetivo: Desarrollar un sistema de alarma que monitoree múltiples sensores con diferentes prioridades.

Funcionalidades:

  • Sensor PIR para detección de movimiento
  • Sensor magnético para puertas y ventanas
  • Botón de pánico de alta prioridad
  • Sistema de armado/desarmado
  • Log de eventos con timestamp
  • Notificaciones diferenciadas por tipo de evento

Aplicación: Seguridad doméstica, control de acceso

3

Control de Motor con Encoder

Avanzado 60 min Mecatrónica

Objetivo: Implementar control de posición de motor usando encoder rotativo con interrupciones de alta frecuencia.

Características avanzadas:

  • Encoder incremental de 2 canales (A/B)
  • Detección de dirección de giro
  • Control PID de posición
  • Límites de software y hardware
  • Compensación de backlash
  • Interfaz de configuración por Serial

Aplicación: CNC, robótica, automation

Aplicaciones Críticas en Industria

Sistemas de Tiempo Real

Las interrupciones son esenciales en aplicaciones industriales donde la respuesta inmediata a eventos críticos puede significar la diferencia entre operación segura y accidentes costosos.

Aplicaciones Críticas:
  • Paradas de emergencia - Respuesta < 1ms
  • Detección de fallos - Protección de equipos
  • Control de velocidad - Feedback instantáneo
  • Posicionamiento preciso - Control servos
Monitoreo en Tiempo Real:
  • Vibración de máquinas - Mantenimiento predictivo
  • Temperatura crítica - Protección térmica
  • Detección de objetos - Sistemas de visión
  • Adquisición de datos - Muestreo sincronizado
Casos de Uso Específicos
Brazo Robótico
Encoders y finales de carrera con respuesta < 100μs
Línea de Producción
Sensores de posición y contadores de alta velocidad
Sistema de Seguridad
Múltiples sensores con prioridades diferenciadas

Mejores Prácticas y Optimización

Diseño de ISRs Eficientes

Reglas Fundamentales
  • Brevedad: ISR máximo 10-50 μs
  • Variables volatile: Para datos compartidos
  • IRAM_ATTR: Para ISRs críticas en RAM
  • Atomic operations: Para variables de 32 bits
  • No malloc/free: En rutinas de interrupción
Patrones Recomendados
  • Set flag + process in loop()
  • FreeRTOS queues para eventos
  • Semaphores para sincronización
  • Ring buffers para datos continuos

Problemas Comunes

ISR demasiado larga

Síntomas: Sistema lento, watchdog reset, pérdida de interrupciones

Solución: Mover lógica compleja al loop principal

Condiciones de carrera

Causa: Acceso concurrente a variables compartidas

Prevención: Usar portENTER_CRITICAL/portEXIT_CRITICAL

Rebote de contactos

Problema: Múltiples interrupciones por un solo evento

Solución: Debounce por software o hardware

Desbordamiento de stack

Causa: ISRs anidadas o recursivas

Prevención: Limitar profundidad, usar IRAM_ATTR

Referencias y Recursos Adicionales