Módulo 3

Comparadores analógicos

Periféricos Analógicos

ESP32 Mecatrónica IoT UNAM

Fundamentos de Comparadores Analógicos

Los comparadores analógicos son circuitos electrónicos fundamentales que realizan la comparación entre dos señales analógicas y generan una salida digital binaria. Son componentes esenciales en sistemas mecatrónicos donde se requiere tomar decisiones lógicas basadas en niveles de señales analógicas.

Principio de Funcionamiento

Un comparador analógico evalúa dos voltajes de entrada:

  • V+ (Entrada no inversora): Señal de entrada principal
  • V- (Entrada inversora): Voltaje de referencia (threshold)
  • Vout: Salida digital (HIGH/LOW)

Función de transferencia:
Vout = HIGH si V+ > V-
Vout = LOW si V+ ≤ V-

Comparación Analógica

Aplicaciones en Mecatrónica Industrial
  • Sistemas de protección por sobre/sub voltaje
  • Detectores de umbral para sensores
  • Control ON/OFF por temperatura
  • Sistemas de emergencia por presión
  • Conversores PWM a señales digitales
  • Sistemas de ventanas de comparación
  • Detectores de cruce por cero
  • Interfaces de sensores industriales

Comparadores Analógicos en ESP32

Nota Importante

El ESP32 no tiene comparadores analógicos dedicados en hardware. Sin embargo, podemos implementar la funcionalidad de comparación utilizando el ADC y lógica de software para crear comparadores digitales eficientes.

Métodos de Implementación

Método Descripción Precisión
Software SimpleComparación directa ADC12 bits
Con HistéresisVentanas de comparación12 bits + filtrado
Filtrado DigitalAnti-ruido avanzadoAlta estabilidad
Hardware ExternoComparadores dedicadosMuy alta velocidad

Consideraciones de Diseño

  • Ruido: Implementar filtrado para señales estables
  • Histéresis: Evitar oscilaciones en el umbral
  • Velocidad: Balance entre precisión y rapidez
  • Referencias: Estabilidad del voltaje de referencia

Tipos de Comparadores Implementables

Comparador Simple

V+ > V-

Ventana de Comparación

V_min < V+ < V_max

Con Histéresis

Umbrales diferenciados

Implementación Práctica

Comparador Simple por Software

C++ - Comparador Básico
/**
 * Comparador Analógico Simple en ESP32
 * Autor: Curso ESP32 Mecatrónica UNAM
 * Implementación por software de comparador analógico
 */

#include "driver/adc.h"
#include "esp_adc_cal.h"

// Configuración del ADC
esp_adc_cal_characteristics_t adc_chars;
const adc_channel_t INPUT_CHANNEL = ADC_CHANNEL_0;    // GPIO36 - Señal de entrada
const adc_channel_t REFERENCE_CHANNEL = ADC_CHANNEL_3; // GPIO39 - Referencia

// Pin de salida digital
const int OUTPUT_PIN = 2;

// Variables de comparación
struct ComparatorState {
    uint32_t input_value;
    uint32_t reference_value;
    float input_voltage;
    float reference_voltage;
    bool output_state;
    unsigned long last_update;
};

ComparatorState comparator;

void setup() {
    Serial.begin(115200);
    
    // Configurar ADC
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(INPUT_CHANNEL, ADC_ATTEN_DB_11);
    adc1_config_channel_atten(REFERENCE_CHANNEL, ADC_ATTEN_DB_11);
    
    // Calibrar ADC
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                            ADC_WIDTH_BIT_12, 1100, &adc_chars);
    
    // Configurar pin de salida
    pinMode(OUTPUT_PIN, OUTPUT);
    digitalWrite(OUTPUT_PIN, LOW);
    
    Serial.println("=== COMPARADOR ANALÓGICO ESP32 ===");
    Serial.println("Entrada: GPIO36 (ADC1_CH0)");
    Serial.println("Referencia: GPIO39 (ADC1_CH3)");
    Serial.println("Salida: GPIO2");
    Serial.println("Lógica: OUT = HIGH si INPUT > REFERENCE");
    Serial.println("=====================================");
}

void loop() {
    updateComparator();
    printComparatorStatus();
    delay(100);
}

void updateComparator() {
    // Leer valores ADC con promediado para reducir ruido
    comparator.input_value = readADCAverage(INPUT_CHANNEL, 10);
    comparator.reference_value = readADCAverage(REFERENCE_CHANNEL, 10);
    
    // Convertir a voltajes
    comparator.input_voltage = esp_adc_cal_raw_to_voltage(
        comparator.input_value, &adc_chars) / 1000.0;
    comparator.reference_voltage = esp_adc_cal_raw_to_voltage(
        comparator.reference_value, &adc_chars) / 1000.0;
    
    // Lógica del comparador
    comparator.output_state = comparator.input_voltage > comparator.reference_voltage;
    
    // Actualizar salida digital
    digitalWrite(OUTPUT_PIN, comparator.output_state ? HIGH : LOW);
    comparator.last_update = millis();
}

uint32_t readADCAverage(adc_channel_t channel, int samples) {
    uint32_t sum = 0;
    for (int i = 0; i < samples; i++) {
        sum += adc1_get_raw(channel);
        delayMicroseconds(100);
    }
    return sum / samples;
}

void printComparatorStatus() {
    Serial.printf("Input: %.3fV (%d) | Ref: %.3fV (%d) | Output: %s\n",
                  comparator.input_voltage, comparator.input_value,
                  comparator.reference_voltage, comparator.reference_value,
                  comparator.output_state ? "HIGH" : "LOW");
}

Comparador con Histéresis

C++ - Comparador con Histéresis
/**
 * Comparador con Histéresis para ESP32
 * Evita oscilaciones en el punto de comparación
 * Ideal para aplicaciones industriales con ruido
 */

class HysteresisComparator {
private:
    adc_channel_t input_channel;
    float reference_voltage;
    float hysteresis_voltage;
    bool current_state;
    bool initialized;
    esp_adc_cal_characteristics_t* adc_chars;
    
    // Umbrales calculados
    float upper_threshold;
    float lower_threshold;
    
public:
    HysteresisComparator(adc_channel_t input_ch, float ref_v, float hyst_v, 
                        esp_adc_cal_characteristics_t* adc_cal) {
        input_channel = input_ch;
        reference_voltage = ref_v;
        hysteresis_voltage = hyst_v;
        adc_chars = adc_cal;
        current_state = false;
        initialized = false;
        
        // Calcular umbrales
        upper_threshold = reference_voltage + (hysteresis_voltage / 2.0);
        lower_threshold = reference_voltage - (hysteresis_voltage / 2.0);
        
        // Configurar canal ADC
        adc1_config_channel_atten(input_channel, ADC_ATTEN_DB_11);
    }
    
    bool update() {
        // Leer y convertir valor de entrada
        uint32_t adc_raw = readFilteredADC();
        uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_raw, adc_chars);
        float input_voltage = voltage_mv / 1000.0;
        
        // Lógica de histéresis
        if (!initialized) {
            // Primera lectura - establecer estado inicial
            current_state = input_voltage > reference_voltage;
            initialized = true;
        } else {
            // Aplicar histéresis
            if (current_state) {
                // Estado HIGH - cambiar a LOW solo si baja del umbral inferior
                if (input_voltage < lower_threshold) {
                    current_state = false;
                }
            } else {
                // Estado LOW - cambiar a HIGH solo si supera el umbral superior
                if (input_voltage > upper_threshold) {
                    current_state = true;
                }
            }
        }
        
        return current_state;
    }
    
    float getInputVoltage() {
        uint32_t adc_raw = readFilteredADC();
        uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_raw, adc_chars);
        return voltage_mv / 1000.0;
    }
    
    void printStatus() {
        float input_v = getInputVoltage();
        Serial.printf("Input: %.3fV | Ref: %.3fV ± %.3fV | State: %s\n",
                      input_v, reference_voltage, hysteresis_voltage/2,
                      current_state ? "HIGH" : "LOW");
        Serial.printf("Thresholds: LOW<%.3fV | HIGH>%.3fV\n", 
                      lower_threshold, upper_threshold);
    }
    
private:
    uint32_t readFilteredADC() {
        // Filtro de media móvil
        const int samples = 5;
        uint32_t sum = 0;
        
        for (int i = 0; i < samples; i++) {
            sum += adc1_get_raw(input_channel);
            delayMicroseconds(200);
        }
        
        return sum / samples;
    }
};

// Ejemplo de uso
esp_adc_cal_characteristics_t adc_chars;
HysteresisComparator tempComparator(ADC_CHANNEL_0, 1.5, 0.2, &adc_chars);

void setup() {
    Serial.begin(115200);
    
    // Configurar ADC
    adc1_config_width(ADC_WIDTH_BIT_12);
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                            ADC_WIDTH_BIT_12, 1100, &adc_chars);
    
    Serial.println("=== COMPARADOR CON HISTÉRESIS ===");
    Serial.println("Referencia: 1.5V ± 0.1V");
    Serial.println("=================================");
}

void loop() {
    bool output = tempComparator.update();
    tempComparator.printStatus();
    
    // Usar resultado para control
    digitalWrite(LED_BUILTIN, output);
    
    delay(200);
}

Comparador de Ventana

C++ - Comparador de Ventana
/**
 * Comparador de Ventana para ESP32
 * Detecta si la señal está dentro de un rango específico
 * Aplicación: Control de calidad, sistemas de protección
 */

enum WindowComparatorState {
    BELOW_WINDOW,    // Señal por debajo del rango
    IN_WINDOW,       // Señal dentro del rango
    ABOVE_WINDOW     // Señal por encima del rango
};

class WindowComparator {
private:
    adc_channel_t input_channel;
    float lower_limit;
    float upper_limit;
    WindowComparatorState current_state;
    esp_adc_cal_characteristics_t* adc_chars;
    
    // Estadísticas
    unsigned long state_change_count;
    unsigned long last_state_change;
    
public:
    WindowComparator(adc_channel_t input_ch, float low_limit, float high_limit,
                    esp_adc_cal_characteristics_t* adc_cal) {
        input_channel = input_ch;
        lower_limit = low_limit;
        upper_limit = high_limit;
        adc_chars = adc_cal;
        current_state = IN_WINDOW;
        state_change_count = 0;
        last_state_change = 0;
        
        // Configurar canal ADC
        adc1_config_channel_atten(input_channel, ADC_ATTEN_DB_11);
    }
    
    WindowComparatorState update() {
        float input_voltage = getInputVoltage();
        WindowComparatorState new_state;
        
        // Determinar estado basado en la ventana
        if (input_voltage < lower_limit) {
            new_state = BELOW_WINDOW;
        } else if (input_voltage > upper_limit) {
            new_state = ABOVE_WINDOW;
        } else {
            new_state = IN_WINDOW;
        }
        
        // Detectar cambio de estado
        if (new_state != current_state) {
            current_state = new_state;
            state_change_count++;
            last_state_change = millis();
        }
        
        return current_state;
    }
    
    bool isInWindow() {
        return current_state == IN_WINDOW;
    }
    
    bool isBelowWindow() {
        return current_state == BELOW_WINDOW;
    }
    
    bool isAboveWindow() {
        return current_state == ABOVE_WINDOW;
    }
    
    float getInputVoltage() {
        uint32_t adc_raw = readStableADC();
        uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_raw, adc_chars);
        return voltage_mv / 1000.0;
    }
    
    String getStateString() {
        switch (current_state) {
            case BELOW_WINDOW: return "BELOW";
            case IN_WINDOW: return "IN_RANGE";
            case ABOVE_WINDOW: return "ABOVE";
            default: return "UNKNOWN";
        }
    }
    
    void printDetailedStatus() {
        float input_v = getInputVoltage();
        float window_center = (upper_limit + lower_limit) / 2.0;
        float window_width = upper_limit - lower_limit;
        
        Serial.println("========== WINDOW COMPARATOR STATUS ==========");
        Serial.printf("Input Voltage: %.3fV\n", input_v);
        Serial.printf("Window: %.3fV - %.3fV (Center: %.3fV, Width: %.3fV)\n",
                      lower_limit, upper_limit, window_center, window_width);
        Serial.printf("Current State: %s\n", getStateString().c_str());
        Serial.printf("State Changes: %lu (Last: %lu ms ago)\n",
                      state_change_count, millis() - last_state_change);
        
        // Indicadores visuales
        if (isInWindow()) {
            Serial.println("Status: ✅ WITHIN ACCEPTABLE RANGE");
        } else if (isBelowWindow()) {
            Serial.printf("Status: ⬇️  BELOW RANGE (%.3fV under)\n", 
                         lower_limit - input_v);
        } else {
            Serial.printf("Status: ⬆️  ABOVE RANGE (%.3fV over)\n", 
                         input_v - upper_limit);
        }
        Serial.println("=============================================\n");
    }
    
private:
    uint32_t readStableADC() {
        // Lectura con filtrado avanzado
        const int samples = 15;
        uint32_t readings[samples];
        
        // Tomar múltiples muestras
        for (int i = 0; i < samples; i++) {
            readings[i] = adc1_get_raw(input_channel);
            delayMicroseconds(100);
        }
        
        // Ordenamiento burbuja simple para filtro de mediana
        for (int i = 0; i < samples - 1; i++) {
            for (int j = 0; j < samples - i - 1; j++) {
                if (readings[j] > readings[j + 1]) {
                    uint32_t temp = readings[j];
                    readings[j] = readings[j + 1];
                    readings[j + 1] = temp;
                }
            }
        }
        
        // Retornar mediana
        return readings[samples / 2];
    }
};

// Ejemplo de aplicación: Control de calidad de voltaje
esp_adc_cal_characteristics_t adc_chars;
WindowComparator qualityControl(ADC_CHANNEL_0, 2.8, 3.2, &adc_chars);

const int ALARM_PIN = 2;
const int OK_LED_PIN = 4;

void setup() {
    Serial.begin(115200);
    
    // Configurar ADC
    adc1_config_width(ADC_WIDTH_BIT_12);
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                            ADC_WIDTH_BIT_12, 1100, &adc_chars);
    
    // Configurar pines de salida
    pinMode(ALARM_PIN, OUTPUT);
    pinMode(OK_LED_PIN, OUTPUT);
    
    Serial.println("=== SISTEMA DE CONTROL DE CALIDAD ===");
    Serial.println("Ventana aceptable: 2.8V - 3.2V");
    Serial.println("Alarma activa fuera del rango");
    Serial.println("====================================");
}

void loop() {
    WindowComparatorState state = qualityControl.update();
    
    // Control de indicadores
    if (qualityControl.isInWindow()) {
        digitalWrite(OK_LED_PIN, HIGH);
        digitalWrite(ALARM_PIN, LOW);
    } else {
        digitalWrite(OK_LED_PIN, LOW);
        digitalWrite(ALARM_PIN, HIGH);  // Alarma activa
    }
    
    // Status detallado cada 2 segundos
    static unsigned long last_detailed_print = 0;
    if (millis() - last_detailed_print > 2000) {
        qualityControl.printDetailedStatus();
        last_detailed_print = millis();
    }
    
    delay(100);
}

Ejercicios Prácticos Interactivos

01
Control Automático de Iluminación
Básico 25-35 min

Objetivo: Crear un sistema de iluminación automática que encienda LEDs cuando la luz ambiente sea inferior a un umbral configurable.

Aplicación: Sistemas de iluminación inteligente en edificios industriales.

Materiales Necesarios
  • ESP32 DevKit
  • Fotoresistor LDR (5-20kΩ)
  • Resistor de referencia 10kΩ
  • LEDs de alta luminosidad (x3)
  • Resistores 220Ω para LEDs
  • Potenciómetro 10kΩ (ajuste umbral)
Sistema de Iluminación Automática
/**
 * Ejercicio 1: Control Automático de Iluminación
 * Sistema que activa iluminación basado en nivel de luz ambiente
 */

#include "driver/adc.h"
#include "esp_adc_cal.h"

esp_adc_cal_characteristics_t adc_chars;

// Configuración de pines
const adc_channel_t LDR_CHANNEL = ADC_CHANNEL_0;        // GPIO36
const adc_channel_t THRESHOLD_CHANNEL = ADC_CHANNEL_3;  // GPIO39
const int LED_PINS[] = {2, 4, 16};  // 3 LEDs de salida
const int NUM_LEDS = 3;

// Sistema de comparación con histéresis
class LightComparator {
private:
    float hysteresis_margin = 0.1;  // 100mV de histéresis
    bool lights_on = false;
    
public:
    bool shouldTurnOn(float ambient, float threshold) {
        if (!lights_on) {
            // Luces apagadas - encender si está muy oscuro
            return ambient < (threshold - hysteresis_margin);
        } else {
            // Luces encendidas - apagar solo si hay suficiente luz
            return ambient < (threshold + hysteresis_margin);
        }
    }
    
    void setState(bool state) { lights_on = state; }
    bool getState() { return lights_on; }
};

LightComparator lightController;

void setup() {
    Serial.begin(115200);
    
    // Configurar ADC
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(LDR_CHANNEL, ADC_ATTEN_DB_11);
    adc1_config_channel_atten(THRESHOLD_CHANNEL, ADC_ATTEN_DB_11);
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                            ADC_WIDTH_BIT_12, 1100, &adc_chars);
    
    // Configurar LEDs
    for (int i = 0; i < NUM_LEDS; i++) {
        pinMode(LED_PINS[i], OUTPUT);
        digitalWrite(LED_PINS[i], LOW);
    }
    
    Serial.println("=== SISTEMA DE ILUMINACIÓN AUTOMÁTICA ===");
    Serial.println("LDR: GPIO36 | Umbral: GPIO39");
    Serial.println("LEDs: GPIO2, GPIO4, GPIO16");
    Serial.println("========================================");
}

void loop() {
    // Leer sensores con filtrado
    float ambient_light = readVoltageFiltered(LDR_CHANNEL);
    float threshold_voltage = readVoltageFiltered(THRESHOLD_CHANNEL);
    
    // Determinar estado de iluminación
    bool should_light = lightController.shouldTurnOn(ambient_light, threshold_voltage);
    lightController.setState(should_light);
    
    // Controlar LEDs con efecto gradual
    controlLEDs(should_light);
    
    // Mostrar estado
    printSystemStatus(ambient_light, threshold_voltage, should_light);
    
    delay(200);
}

float readVoltageFiltered(adc_channel_t channel) {
    uint32_t adc_sum = 0;
    const int samples = 8;
    
    for (int i = 0; i < samples; i++) {
        adc_sum += adc1_get_raw(channel);
        delayMicroseconds(500);
    }
    
    uint32_t adc_avg = adc_sum / samples;
    uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_avg, &adc_chars);
    return voltage_mv / 1000.0;
}

void controlLEDs(bool enable) {
    static unsigned long last_update = 0;
    static int led_index = 0;
    static bool transitioning = false;
    
    if (enable) {
        // Encender LEDs secuencialmente para efecto visual
        if (millis() - last_update > 100) {
            digitalWrite(LED_PINS[led_index], HIGH);
            led_index = (led_index + 1) % NUM_LEDS;
            
            if (led_index == 0 && !transitioning) {
                // Todos encendidos
                for (int i = 0; i < NUM_LEDS; i++) {
                    digitalWrite(LED_PINS[i], HIGH);
                }
            }
            last_update = millis();
        }
    } else {
        // Apagar todos los LEDs
        for (int i = 0; i < NUM_LEDS; i++) {
            digitalWrite(LED_PINS[i], LOW);
        }
        led_index = 0;
    }
}

void printSystemStatus(float ambient, float threshold, bool lights_on) {
    // Convertir voltajes a porcentajes de luz
    float ambient_percent = (ambient / 3.3) * 100;
    float threshold_percent = (threshold / 3.3) * 100;
    
    Serial.printf("Luz ambiente: %.2fV (%.1f%%) | ", ambient, ambient_percent);
    Serial.printf("Umbral: %.2fV (%.1f%%) | ", threshold, threshold_percent);
    Serial.printf("Iluminación: %s", lights_on ? "🔆 ON" : "🌙 OFF");
    
    if (lights_on && ambient < threshold) {
        Serial.println(" [MODO NOCTURNO]");
    } else if (!lights_on && ambient >= threshold) {
        Serial.println(" [LUZ SUFICIENTE]");
    } else {
        Serial.println(" [TRANSICIÓN]");
    }
}
02
Sistema de Protección por Temperatura
Intermedio 35-45 min

Objetivo: Implementar un sistema de protección que active ventilación de emergencia cuando la temperatura supere límites críticos.

Aplicación: Sistemas de seguridad térmica en equipos industriales.

Materiales Necesarios
  • ESP32 DevKit
  • Sensor temperatura TMP36
  • Potenciómetro 10kΩ (setpoint)
  • Relé 5V (control ventilador)
  • LED rojo (alarma) + resistor
  • Buzzer 5V (opcional)
  • Transistor 2N2222 para relé
Protección Térmica Industrial
/**
 * Ejercicio 2: Sistema de Protección por Temperatura
 * Control de seguridad térmica con múltiples niveles de alarma
 */

#include "driver/adc.h"
#include "esp_adc_cal.h"

esp_adc_cal_characteristics_t adc_chars;

// Configuración de pines
const adc_channel_t TEMP_CHANNEL = ADC_CHANNEL_0;      // GPIO36 - TMP36
const adc_channel_t SETPOINT_CHANNEL = ADC_CHANNEL_3;  // GPIO39 - Setpoint
const int FAN_RELAY_PIN = 2;     // Control de ventilador
const int ALARM_LED_PIN = 4;     // LED de alarma
const int BUZZER_PIN = 16;       // Buzzer de alarma
const int STATUS_LED_PIN = 17;   // LED de estado normal

// Estados del sistema de protección
enum ThermalProtectionState {
    NORMAL,           // Temperatura normal
    WARNING,          // Temperatura elevada
    CRITICAL,         // Temperatura crítica
    EMERGENCY         // Emergencia térmica
};

class ThermalProtectionSystem {
private:
    ThermalProtectionState current_state;
    float warning_offset = 5.0;    // °C sobre setpoint
    float critical_offset = 10.0;  // °C sobre setpoint
    float emergency_offset = 15.0; // °C sobre setpoint
    
    // Histéresis para cada nivel
    float hysteresis = 2.0;  // °C
    
    unsigned long state_change_time;
    unsigned long alarm_start_time;
    bool alarm_active;
    
public:
    ThermalProtectionSystem() {
        current_state = NORMAL;
        state_change_time = 0;
        alarm_start_time = 0;
        alarm_active = false;
    }
    
    ThermalProtectionState evaluateTemperature(float temp, float setpoint) {
        ThermalProtectionState new_state = current_state;
        
        // Calcular umbrales con histéresis
        float warning_temp = setpoint + warning_offset;
        float critical_temp = setpoint + critical_offset;
        float emergency_temp = setpoint + emergency_offset;
        
        // Lógica de transición de estados con histéresis
        switch (current_state) {
            case NORMAL:
                if (temp > warning_temp) new_state = WARNING;
                break;
                
            case WARNING:
                if (temp < warning_temp - hysteresis) {
                    new_state = NORMAL;
                } else if (temp > critical_temp) {
                    new_state = CRITICAL;
                }
                break;
                
            case CRITICAL:
                if (temp < critical_temp - hysteresis) {
                    new_state = WARNING;
                } else if (temp > emergency_temp) {
                    new_state = EMERGENCY;
                }
                break;
                
            case EMERGENCY:
                if (temp < emergency_temp - hysteresis) {
                    new_state = CRITICAL;
                }
                break;
        }
        
        // Actualizar estado si cambió
        if (new_state != current_state) {
            current_state = new_state;
            state_change_time = millis();
            
            if (current_state >= WARNING && !alarm_active) {
                alarm_start_time = millis();
                alarm_active = true;
            } else if (current_state == NORMAL) {
                alarm_active = false;
            }
        }
        
        return current_state;
    }
    
    String getStateString() {
        switch (current_state) {
            case NORMAL: return "NORMAL";
            case WARNING: return "WARNING";
            case CRITICAL: return "CRITICAL";
            case EMERGENCY: return "EMERGENCY";
            default: return "UNKNOWN";
        }
    }
    
    bool shouldActivateFan() {
        return current_state >= WARNING;
    }
    
    bool shouldActivateAlarm() {
        return current_state >= CRITICAL;
    }
    
    bool isEmergency() {
        return current_state == EMERGENCY;
    }
    
    unsigned long getTimeSinceStateChange() {
        return millis() - state_change_time;
    }
    
    unsigned long getAlarmDuration() {
        return alarm_active ? millis() - alarm_start_time : 0;
    }
};

ThermalProtectionSystem thermalSystem;

void setup() {
    Serial.begin(115200);
    
    // Configurar ADC
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(TEMP_CHANNEL, ADC_ATTEN_DB_11);
    adc1_config_channel_atten(SETPOINT_CHANNEL, ADC_ATTEN_DB_11);
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                            ADC_WIDTH_BIT_12, 1100, &adc_chars);
    
    // Configurar pines de salida
    pinMode(FAN_RELAY_PIN, OUTPUT);
    pinMode(ALARM_LED_PIN, OUTPUT);
    pinMode(BUZZER_PIN, OUTPUT);
    pinMode(STATUS_LED_PIN, OUTPUT);
    
    // Estado inicial
    digitalWrite(FAN_RELAY_PIN, LOW);
    digitalWrite(ALARM_LED_PIN, LOW);
    digitalWrite(BUZZER_PIN, LOW);
    digitalWrite(STATUS_LED_PIN, HIGH);
    
    Serial.println("=== SISTEMA DE PROTECCIÓN TÉRMICA ===");
    Serial.println("TMP36: GPIO36 | Setpoint: GPIO39");
    Serial.println("Ventilador: GPIO2 | Alarma: GPIO4");
    Serial.println("Niveles: +5°C Warning, +10°C Critical, +15°C Emergency");
    Serial.println("====================================");
}

void loop() {
    // Leer temperatura y setpoint
    float temperature = readTemperatureTMP36();
    float setpoint = readSetpoint();
    
    // Evaluar estado térmico
    ThermalProtectionState state = thermalSystem.evaluateTemperature(temperature, setpoint);
    
    // Controlar actuadores
    controlOutputs(state);
    
    // Mostrar estado detallado
    printThermalStatus(temperature, setpoint, state);
    
    delay(500);
}

float readTemperatureTMP36() {
    // Leer TMP36 con filtrado
    uint32_t adc_sum = 0;
    const int samples = 10;
    
    for (int i = 0; i < samples; i++) {
        adc_sum += adc1_get_raw(TEMP_CHANNEL);
        delayMicroseconds(1000);
    }
    
    uint32_t adc_avg = adc_sum / samples;
    uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_avg, &adc_chars);
    
    // Conversión TMP36: 10mV/°C, 500mV offset a 0°C
    float temperature = (voltage_mv - 500.0) / 10.0;
    return temperature;
}

float readSetpoint() {
    // Convertir potenciómetro a rango de temperatura (20-60°C)
    uint32_t adc_raw = adc1_get_raw(SETPOINT_CHANNEL);
    uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_raw, &adc_chars);
    
    // Mapear 0-3.3V a 20-60°C
    float setpoint = 20.0 + (voltage_mv / 3300.0) * 40.0;
    return setpoint;
}

void controlOutputs(ThermalProtectionState state) {
    static unsigned long last_blink = 0;
    static bool blink_state = false;
    
    // Control del ventilador
    digitalWrite(FAN_RELAY_PIN, thermalSystem.shouldActivateFan() ? HIGH : LOW);
    
    // Control del LED de estado
    digitalWrite(STATUS_LED_PIN, (state == NORMAL) ? HIGH : LOW);
    
    // Control de alarmas con patrones de parpadeo
    if (thermalSystem.shouldActivateAlarm()) {
        // Parpadeo rápido para critical, muy rápido para emergency
        int blink_interval = (state == EMERGENCY) ? 100 : 300;
        
        if (millis() - last_blink > blink_interval) {
            blink_state = !blink_state;
            digitalWrite(ALARM_LED_PIN, blink_state ? HIGH : LOW);
            
            // Buzzer solo en emergency
            if (state == EMERGENCY) {
                digitalWrite(BUZZER_PIN, blink_state ? HIGH : LOW);
            }
            
            last_blink = millis();
        }
    } else {
        // Apagar alarmas
        digitalWrite(ALARM_LED_PIN, LOW);
        digitalWrite(BUZZER_PIN, LOW);
    }
}

void printThermalStatus(float temp, float setpoint, ThermalProtectionState state) {
    Serial.println("========== ESTADO TÉRMICO ==========");
    Serial.printf("Temperatura: %.2f°C\n", temp);
    Serial.printf("Setpoint: %.2f°C\n", setpoint);
    Serial.printf("Diferencia: %+.2f°C\n", temp - setpoint);
    Serial.printf("Estado: %s\n", thermalSystem.getStateString().c_str());
    
    // Tiempo en estado actual
    unsigned long time_in_state = thermalSystem.getTimeSinceStateChange();
    Serial.printf("Tiempo en estado: %lu seg\n", time_in_state / 1000);
    
    // Estado de actuadores
    Serial.printf("Ventilador: %s\n", thermalSystem.shouldActivateFan() ? "🌀 ACTIVO" : "⭕ OFF");
    
    if (thermalSystem.shouldActivateAlarm()) {
        unsigned long alarm_time = thermalSystem.getAlarmDuration();
        Serial.printf("🚨 ALARMA ACTIVA (%lu seg)\n", alarm_time / 1000);
    }
    
    // Indicadores de estado
    switch (state) {
        case NORMAL:
            Serial.println("Status: ✅ Temperatura normal");
            break;
        case WARNING:
            Serial.println("Status: ⚠️  Temperatura elevada - Ventilación activada");
            break;
        case CRITICAL:
            Serial.println("Status: 🔥 Temperatura crítica - Alarma activa");
            break;
        case EMERGENCY:
            Serial.println("Status: 🆘 EMERGENCIA TÉRMICA - Protocolo de seguridad");
            break;
    }
    
    Serial.println("==================================\n");
}
03
Monitor de Calidad de Energía
Avanzado 50-70 min

Objetivo: Crear un sistema de monitoreo que detecte variaciones de voltaje fuera de rangos aceptables y registre eventos de calidad de energía.

Aplicación: Sistemas de monitoreo de calidad eléctrica en instalaciones industriales.

Materiales Necesarios
  • ESP32 DevKit
  • Divisores de voltaje precision (resistors 0.1%)
  • Transformador de voltaje (para medición AC)
  • Capacitores de filtrado
  • Display LCD 16x2 I2C
  • LEDs indicadores (Verde/Amarillo/Rojo)
  • Tarjeta microSD para logging
Sistema de Calidad de Energía
/**
 * Ejercicio 3: Monitor de Calidad de Energía
 * Sistema avanzado de monitoreo con múltiples comparadores
 * y registro de eventos
 */

#include "driver/adc.h"
#include "esp_adc_cal.h"
#include 
#include 

esp_adc_cal_characteristics_t adc_chars;

// Canales de medición
const adc_channel_t VOLTAGE_CHANNEL = ADC_CHANNEL_0;    // GPIO36
const adc_channel_t CURRENT_CHANNEL = ADC_CHANNEL_3;    // GPIO39
const adc_channel_t REFERENCE_CHANNEL = ADC_CHANNEL_6;  // GPIO34

// Pines de indicadores
const int GREEN_LED = 2;      // Operación normal
const int YELLOW_LED = 4;     // Advertencia
const int RED_LED = 16;       // Falla crítica
const int BUZZER_PIN = 17;    // Alarma sonora

// Estándares de calidad de energía (IEEE 519)
struct PowerQualityLimits {
    float nominal_voltage = 120.0;      // Voltaje nominal (V)
    float voltage_tolerance = 0.05;     // ±5%
    float voltage_warning = 0.08;       // ±8%
    float voltage_critical = 0.10;      // ±10%
    
    float nominal_frequency = 60.0;     // Frecuencia nominal (Hz)
    float frequency_tolerance = 0.1;    // ±0.1 Hz
    
    float thd_warning = 0.05;           // 5% THD
    float thd_critical = 0.08;          // 8% THD
};

enum PowerQualityEvent {
    NORMAL_OPERATION,
    VOLTAGE_SAG,        // Subtensión
    VOLTAGE_SWELL,      // Sobretensión  
    VOLTAGE_INTERRUPTION, // Interrupción
    FREQUENCY_DEVIATION,  // Desviación de frecuencia
    HARMONIC_DISTORTION  // Distorsión armónica
};

struct PowerQualityData {
    float voltage_rms;
    float current_rms;
    float frequency;
    float thd_voltage;
    float power_factor;
    PowerQualityEvent active_event;
    unsigned long event_start_time;
    unsigned long event_duration;
};

class PowerQualityMonitor {
private:
    PowerQualityLimits limits;
    PowerQualityData current_data;
    
    // Buffers para análisis
    static const int SAMPLE_BUFFER_SIZE = 128;
    float voltage_buffer[SAMPLE_BUFFER_SIZE];
    float current_buffer[SAMPLE_BUFFER_SIZE];
    int buffer_index;
    
    // Estadísticas de eventos
    struct EventStatistics {
        unsigned long total_events;
        unsigned long sag_events;
        unsigned long swell_events;
        unsigned long interruption_events;
        unsigned long frequency_events;
        unsigned long harmonic_events;
        unsigned long last_event_time;
    } event_stats;
    
    // Estado del monitor
    bool monitoring_active;
    unsigned long last_analysis_time;
    unsigned long analysis_interval = 1000; // 1 segundo
    
public:
    PowerQualityMonitor() {
        buffer_index = 0;
        monitoring_active = false;
        last_analysis_time = 0;
        memset(&event_stats, 0, sizeof(event_stats));
        current_data.active_event = NORMAL_OPERATION;
        current_data.event_start_time = 0;
    }
    
    void initialize() {
        // Configurar ADC con alta precisión
        adc1_config_width(ADC_WIDTH_BIT_12);
        adc1_config_channel_atten(VOLTAGE_CHANNEL, ADC_ATTEN_DB_11);
        adc1_config_channel_atten(CURRENT_CHANNEL, ADC_ATTEN_DB_11);
        adc1_config_channel_atten(REFERENCE_CHANNEL, ADC_ATTEN_DB_11);
        
        esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                                ADC_WIDTH_BIT_12, 1100, &adc_chars);
        
        monitoring_active = true;
        Serial.println("Monitor de Calidad de Energía inicializado");
    }
    
    void update() {
        if (!monitoring_active) return;
        
        // Adquisición continua de muestras
        acquireSamples();
        
        // Análisis periódico
        if (millis() - last_analysis_time >= analysis_interval) {
            performPowerQualityAnalysis();
            detectPowerQualityEvents();
            last_analysis_time = millis();
        }
    }
    
private:
    void acquireSamples() {
        // Muestreo de voltaje y corriente
        uint32_t voltage_adc = adc1_get_raw(VOLTAGE_CHANNEL);
        uint32_t current_adc = adc1_get_raw(CURRENT_CHANNEL);
        
        // Convertir a valores reales
        uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(voltage_adc, &adc_chars);
        uint32_t current_mv = esp_adc_cal_raw_to_voltage(current_adc, &adc_chars);
        
        // Escalado para voltaje y corriente reales
        voltage_buffer[buffer_index] = (voltage_mv / 1000.0) * 50.0; // Factor de escala del divisor
        current_buffer[buffer_index] = (current_mv / 1000.0) * 10.0; // Factor de escala del sensor
        
        buffer_index = (buffer_index + 1) % SAMPLE_BUFFER_SIZE;
    }
    
    void performPowerQualityAnalysis() {
        // Calcular RMS de voltaje
        current_data.voltage_rms = calculateRMS(voltage_buffer, SAMPLE_BUFFER_SIZE);
        current_data.current_rms = calculateRMS(current_buffer, SAMPLE_BUFFER_SIZE);
        
        // Calcular frecuencia (simulada - requiere análisis de cruces por cero)
        current_data.frequency = estimateFrequency();
        
        // Calcular THD (Total Harmonic Distortion) simplificado
        current_data.thd_voltage = calculateTHD(voltage_buffer, SAMPLE_BUFFER_SIZE);
        
        // Calcular factor de potencia (simulado)
        current_data.power_factor = calculatePowerFactor();
    }
    
    float calculateRMS(float* buffer, int size) {
        float sum_squares = 0;
        for (int i = 0; i < size; i++) {
            sum_squares += buffer[i] * buffer[i];
        }
        return sqrt(sum_squares / size);
    }
    
    float estimateFrequency() {
        // Implementación simplificada - detecta cruces por cero
        int zero_crossings = 0;
        float last_sample = voltage_buffer[0];
        
        for (int i = 1; i < SAMPLE_BUFFER_SIZE; i++) {
            if ((last_sample < 0 && voltage_buffer[i] >= 0) ||
                (last_sample >= 0 && voltage_buffer[i] < 0)) {
                zero_crossings++;
            }
            last_sample = voltage_buffer[i];
        }
        
        // Frecuencia estimada basada en cruces por cero
        float sample_rate = 1000.0; // Hz (asumiendo 1ms entre muestras)
        float estimated_freq = (zero_crossings * sample_rate) / (2 * SAMPLE_BUFFER_SIZE);
        
        return estimated_freq > 0 ? estimated_freq : limits.nominal_frequency;
    }
    
    float calculateTHD(float* buffer, int size) {
        // THD simplificado - mide distorsión respecto a senoidal ideal
        float fundamental_power = 0;
        float total_power = 0;
        
        // Análisis simplificado de armónicos
        for (int i = 0; i < size; i++) {
            float sample = buffer[i];
            total_power += sample * sample;
            
            // Aproximación del fundamental (muy simplificada)
            float ideal_sine = limits.nominal_voltage * sin(2 * M_PI * limits.nominal_frequency * i / 1000.0);
            fundamental_power += ideal_sine * ideal_sine;
        }
        
        if (fundamental_power > 0) {
            return sqrt((total_power - fundamental_power) / fundamental_power);
        }
        return 0;
    }
    
    float calculatePowerFactor() {
        // Factor de potencia simplificado (requiere análisis de fase)
        // Por simplicidad, retornamos un valor basado en la calidad de la forma de onda
        float distortion_factor = 1.0 - current_data.thd_voltage;
        return constrain(distortion_factor, 0.7, 1.0);
    }
    
    void detectPowerQualityEvents() {
        PowerQualityEvent new_event = NORMAL_OPERATION;
        
        // Análisis de voltaje
        float voltage_deviation = abs(current_data.voltage_rms - limits.nominal_voltage) / limits.nominal_voltage;
        
        if (current_data.voltage_rms < limits.nominal_voltage * 0.1) {
            new_event = VOLTAGE_INTERRUPTION;
        } else if (voltage_deviation > limits.voltage_critical) {
            if (current_data.voltage_rms > limits.nominal_voltage) {
                new_event = VOLTAGE_SWELL;
            } else {
                new_event = VOLTAGE_SAG;
            }
        }
        
        // Análisis de frecuencia
        float freq_deviation = abs(current_data.frequency - limits.nominal_frequency);
        if (freq_deviation > limits.frequency_tolerance) {
            new_event = FREQUENCY_DEVIATION;
        }
        
        // Análisis de THD
        if (current_data.thd_voltage > limits.thd_critical) {
            new_event = HARMONIC_DISTORTION;
        }
        
        // Gestión de eventos
        if (new_event != current_data.active_event) {
            if (current_data.active_event != NORMAL_OPERATION) {
                // Finalizar evento anterior
                current_data.event_duration = millis() - current_data.event_start_time;
                logPowerQualityEvent(current_data.active_event, current_data.event_duration);
            }
            
            // Iniciar nuevo evento
            current_data.active_event = new_event;
            current_data.event_start_time = millis();
            
            if (new_event != NORMAL_OPERATION) {
                event_stats.total_events++;
                event_stats.last_event_time = millis();
                updateEventStatistics(new_event);
            }
        }
    }
    
    void updateEventStatistics(PowerQualityEvent event) {
        switch (event) {
            case VOLTAGE_SAG: event_stats.sag_events++; break;
            case VOLTAGE_SWELL: event_stats.swell_events++; break;
            case VOLTAGE_INTERRUPTION: event_stats.interruption_events++; break;
            case FREQUENCY_DEVIATION: event_stats.frequency_events++; break;
            case HARMONIC_DISTORTION: event_stats.harmonic_events++; break;
            default: break;
        }
    }
    
    void logPowerQualityEvent(PowerQualityEvent event, unsigned long duration) {
        Serial.println("========== EVENTO DE CALIDAD DE ENERGÍA ==========");
        Serial.printf("Tipo: %s\n", getEventString(event).c_str());
        Serial.printf("Duración: %lu ms\n", duration);
        Serial.printf("Voltaje RMS: %.2f V\n", current_data.voltage_rms);
        Serial.printf("Frecuencia: %.2f Hz\n", current_data.frequency);
        Serial.printf("THD: %.2f%%\n", current_data.thd_voltage * 100);
        Serial.println("===============================================");
    }
    
    String getEventString(PowerQualityEvent event) {
        switch (event) {
            case NORMAL_OPERATION: return "Operación Normal";
            case VOLTAGE_SAG: return "Subtensión (SAG)";
            case VOLTAGE_SWELL: return "Sobretensión (SWELL)";
            case VOLTAGE_INTERRUPTION: return "Interrupción de Voltaje";
            case FREQUENCY_DEVIATION: return "Desviación de Frecuencia";
            case HARMONIC_DISTORTION: return "Distorsión Armónica";
            default: return "Evento Desconocido";
        }
    }
    
public:
    void printDetailedStatus() {
        Serial.println("========== ESTADO DE CALIDAD DE ENERGÍA ==========");
        Serial.printf("Voltaje RMS: %.2f V (Nom: %.2f V)\n", 
                      current_data.voltage_rms, limits.nominal_voltage);
        Serial.printf("Corriente RMS: %.2f A\n", current_data.current_rms);
        Serial.printf("Frecuencia: %.2f Hz (Nom: %.2f Hz)\n", 
                      current_data.frequency, limits.nominal_frequency);
        Serial.printf("THD Voltaje: %.2f%% (Límite: %.2f%%)\n", 
                      current_data.thd_voltage * 100, limits.thd_critical * 100);
        Serial.printf("Factor de Potencia: %.3f\n", current_data.power_factor);
        Serial.printf("Evento Actual: %s\n", getEventString(current_data.active_event).c_str());
        
        if (current_data.active_event != NORMAL_OPERATION) {
            unsigned long current_duration = millis() - current_data.event_start_time;
            Serial.printf("Duración del Evento: %lu ms\n", current_duration);
        }
        
        Serial.println("\n--- ESTADÍSTICAS DE EVENTOS ---");
        Serial.printf("Total de Eventos: %lu\n", event_stats.total_events);
        Serial.printf("Subtensiones: %lu\n", event_stats.sag_events);
        Serial.printf("Sobretensiones: %lu\n", event_stats.swell_events);
        Serial.printf("Interrupciones: %lu\n", event_stats.interruption_events);
        Serial.printf("Desviaciones de Frecuencia: %lu\n", event_stats.frequency_events);
        Serial.printf("Distorsiones Armónicas: %lu\n", event_stats.harmonic_events);
        Serial.println("============================================\n");
    }
    
    PowerQualityEvent getCurrentEvent() {
        return current_data.active_event;
    }
    
    PowerQualityData getCurrentData() {
        return current_data;
    }
};

PowerQualityMonitor powerMonitor;

void setup() {
    Serial.begin(115200);
    
    // Configurar pines de indicadores
    pinMode(GREEN_LED, OUTPUT);
    pinMode(YELLOW_LED, OUTPUT);
    pinMode(RED_LED, OUTPUT);
    pinMode(BUZZER_PIN, OUTPUT);
    
    // Estado inicial
    digitalWrite(GREEN_LED, HIGH);
    digitalWrite(YELLOW_LED, LOW);
    digitalWrite(RED_LED, LOW);
    digitalWrite(BUZZER_PIN, LOW);
    
    // Inicializar monitor
    powerMonitor.initialize();
    
    Serial.println("=== SISTEMA DE MONITOREO DE CALIDAD DE ENERGÍA ===");
    Serial.println("Estándares IEEE 519 implementados");
    Serial.println("Monitoreo continuo de voltaje, frecuencia y THD");
    Serial.println("================================================");
}

void loop() {
    // Actualizar monitor de calidad
    powerMonitor.update();
    
    // Controlar indicadores
    controlIndicators();
    
    // Status detallado cada 5 segundos
    static unsigned long last_detailed_status = 0;
    if (millis() - last_detailed_status > 5000) {
        powerMonitor.printDetailedStatus();
        last_detailed_status = millis();
    }
    
    delay(50);  // 20 Hz de actualización
}

void controlIndicators() {
    PowerQualityEvent current_event = powerMonitor.getCurrentEvent();
    PowerQualityData data = powerMonitor.getCurrentData();
    
    // Resetear todos los indicadores
    digitalWrite(GREEN_LED, LOW);
    digitalWrite(YELLOW_LED, LOW);
    digitalWrite(RED_LED, LOW);
    digitalWrite(BUZZER_PIN, LOW);
    
    // Control basado en el tipo de evento
    switch (current_event) {
        case NORMAL_OPERATION:
            digitalWrite(GREEN_LED, HIGH);
            break;
            
        case VOLTAGE_SAG:
        case VOLTAGE_SWELL:
        case FREQUENCY_DEVIATION:
            // Eventos de advertencia - LED amarillo parpadeante
            digitalWrite(YELLOW_LED, (millis() % 1000) < 500);
            break;
            
        case VOLTAGE_INTERRUPTION:
        case HARMONIC_DISTORTION:
            // Eventos críticos - LED rojo y alarma
            digitalWrite(RED_LED, (millis() % 500) < 250);
            digitalWrite(BUZZER_PIN, (millis() % 1000) < 100); // Beep corto
            break;
    }
}

Referencias Técnicas Especializadas

Estándares Industriales
  • IEEE 519: Harmonic Control in Electrical Power Systems
  • IEC 61000-4-30: Power Quality Measurement Methods
  • ANSI C84.1: Voltage Ratings for Electric Power Systems