Módulo 3

ADC: Conversores analógico/digital, resolución, ruido, calibración

Periféricos Analógicos

ESP32 Mecatrónica IoT UNAM

Fundamentos Teóricos de ADC

Los Conversores Analógico-Digital (ADC) son componentes fundamentales que permiten la interfaz entre el mundo analógico continuo y el digital discreto. En sistemas mecatrónicos industriales, estos dispositivos son esenciales para la adquisición de datos de sensores y el monitoreo de variables de proceso.

Principios de Funcionamiento

El proceso de conversión ADC involucra tres etapas principales:

  • Muestreo (Sampling): Captura de valores discretos en el tiempo
  • Cuantización: Asignación de niveles digitales finitos
  • Codificación: Representación binaria del valor cuantizado

Conversión Analógico-Digital

Aplicaciones en Mecatrónica Industrial
  • Control de temperatura en hornos industriales
  • Monitoreo de presión en sistemas neumáticos
  • Medición de velocidad en motores
  • Control de pH en procesos químicos
  • Sistemas de adquisición de datos
  • Control de calidad automatizado
  • Monitoreo de vibraciones
  • Supervisión de líneas de producción

ADC del ESP32: Especificaciones Técnicas

Características Principales

Parámetro Especificación
Resolución12 bits (4096 niveles)
Número de canales18 canales (ADC1: 8, ADC2: 10)
Velocidad de muestreoHasta 2 MSPS
Rango de voltaje0V - 3.3V (configurable)
Precisión típica±2 LSB
Atenuación0dB, 2.5dB, 6dB, 11dB

Consideraciones Importantes

  • ADC2: No disponible cuando WiFi está activo
  • Ruido: Filtrado necesario para aplicaciones precisas
  • No linealidad: Calibración recomendada
  • Impedancia: Fuente debe ser < 10kΩ

Mapeo de Pines ADC

ADC1 (Recomendado)
GPIO36 - ADC1_CH0
GPIO37 - ADC1_CH1
GPIO38 - ADC1_CH2
GPIO39 - ADC1_CH3
GPIO32 - ADC1_CH4
GPIO33 - ADC1_CH5
GPIO34 - ADC1_CH6
GPIO35 - ADC1_CH7
Configuración de Atenuación
AtenuaciónRango Voltaje
0dB0V - 1.1V
2.5dB0V - 1.5V
6dB0V - 2.2V
11dB0V - 3.9V

Implementación Práctica

Configuración Básica del ADC

C++ - ESP32 Arduino IDE
/**
 * Configuración básica del ADC en ESP32
 * Autor: Curso ESP32 Mecatrónica UNAM
 * Fecha: 2024
 */

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

// Calibración del ADC
esp_adc_cal_characteristics_t adc_chars;
const adc_channel_t channel = ADC_CHANNEL_0;  // GPIO36
const adc_atten_t atten = ADC_ATTEN_DB_11;    // Rango 0-3.9V
const adc_unit_t unit = ADC_UNIT_1;

void setup() {
    Serial.begin(115200);
    
    // Configurar ADC1
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(channel, atten);
    
    // Caracterizar ADC para calibración
    esp_adc_cal_characterize(unit, atten, ADC_WIDTH_BIT_12, 
                            1100, &adc_chars);
    
    Serial.println("ADC inicializado correctamente");
    Serial.println("Canal: GPIO36 (ADC1_CH0)");
    Serial.println("Atenuación: 11dB (0-3.9V)");
    Serial.println("Resolución: 12 bits (0-4095)");
    Serial.println("============================");
}

void loop() {
    // Lectura cruda del ADC (0-4095)
    uint32_t adc_raw = adc1_get_raw(channel);
    
    // Conversión a voltaje calibrado (mV)
    uint32_t voltage = esp_adc_cal_raw_to_voltage(adc_raw, &adc_chars);
    
    // Mostrar resultados
    Serial.printf("ADC Raw: %d\t", adc_raw);
    Serial.printf("Voltaje: %.2f V\n", voltage / 1000.0);
    
    delay(500);
}

Filtrado Digital para Reducir Ruido

C++ - Filtro de Media Móvil
/**
 * Implementación de filtro de media móvil para ADC
 * Reduce el ruido en lecturas analógicas
 */

class ADCFilter {
private:
    static const int BUFFER_SIZE = 10;
    uint32_t buffer[BUFFER_SIZE];
    int index;
    bool buffer_full;
    
public:
    ADCFilter() : index(0), buffer_full(false) {
        memset(buffer, 0, sizeof(buffer));
    }
    
    uint32_t addSample(uint32_t new_sample) {
        buffer[index] = new_sample;
        index = (index + 1) % BUFFER_SIZE;
        
        if (!buffer_full && index == 0) {
            buffer_full = true;
        }
        
        return getAverage();
    }
    
    uint32_t getAverage() {
        uint32_t sum = 0;
        int count = buffer_full ? BUFFER_SIZE : index;
        
        for (int i = 0; i < count; i++) {
            sum += buffer[i];
        }
        
        return count > 0 ? sum / count : 0;
    }
};

ADCFilter adc_filter;

void loop() {
    uint32_t raw_reading = adc1_get_raw(ADC_CHANNEL_0);
    uint32_t filtered_reading = adc_filter.addSample(raw_reading);
    
    Serial.printf("Raw: %d\tFiltered: %d\n", raw_reading, filtered_reading);
    delay(100);
}

Calibración Avanzada del ADC

C++ - Calibración Lineal
/**
 * Sistema de calibración avanzada para ADC ESP32
 * Corrige no linealidad y offset del ADC
 */

struct CalibrationPoint {
    float reference_voltage;
    uint32_t adc_reading;
};

class ADCCalibrator {
private:
    CalibrationPoint points[2];  // Calibración de 2 puntos
    bool calibrated;
    float slope;
    float offset;
    
public:
    ADCCalibrator() : calibrated(false) {}
    
    void addCalibrationPoint(int point_index, float ref_voltage, uint32_t adc_value) {
        if (point_index >= 0 && point_index < 2) {
            points[point_index].reference_voltage = ref_voltage;
            points[point_index].adc_reading = adc_value;
            
            if (point_index == 1) {
                calculateLinearCoefficients();
            }
        }
    }
    
private:
    void calculateLinearCoefficients() {
        float x1 = points[0].adc_reading;
        float y1 = points[0].reference_voltage;
        float x2 = points[1].adc_reading;
        float y2 = points[1].reference_voltage;
        
        slope = (y2 - y1) / (x2 - x1);
        offset = y1 - slope * x1;
        calibrated = true;
        
        Serial.println("Calibración completada:");
        Serial.printf("Pendiente: %.6f\n", slope);
        Serial.printf("Offset: %.6f\n", offset);
    }
    
public:
    float getCalibratedVoltage(uint32_t adc_reading) {
        if (calibrated) {
            return slope * adc_reading + offset;
        } else {
            // Usar calibración por defecto si no hay calibración custom
            uint32_t voltage = esp_adc_cal_raw_to_voltage(adc_reading, &adc_chars);
            return voltage / 1000.0;
        }
    }
};

ADCCalibrator calibrator;

void setup() {
    Serial.begin(115200);
    
    // Configuración ADC
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC_CHANNEL_0, ADC_ATTEN_DB_11);
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                            ADC_WIDTH_BIT_12, 1100, &adc_chars);
    
    // Ejemplo de calibración con voltajes conocidos
    // Punto 1: 0V -> lectura ADC típica
    // Punto 2: 3.3V -> lectura ADC típica
    calibrator.addCalibrationPoint(0, 0.0, 0);      // 0V
    calibrator.addCalibrationPoint(1, 3.3, 4095);   // 3.3V
    
    Serial.println("Sistema ADC calibrado listo");
}

Ejercicios Prácticos Interactivos

01
Sensor de Temperatura LM35
Básico 20-30 min

Objetivo: Implementar lectura de temperatura con sensor analógico LM35 y conversión a grados Celsius.

Aplicación: Monitoreo térmico en sistemas de control industrial.

Materiales Necesarios
  • ESP32 DevKit
  • Sensor LM35 (temperatura analógica)
  • Protoboard y cables dupont
  • Resistor 10kΩ (pull-up opcional)
Código Completo - LM35
/**
 * Ejercicio 1: Sensor de Temperatura LM35
 * LM35: 10mV/°C, rango 0-100°C
 */

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

esp_adc_cal_characteristics_t adc_chars;
const int SAMPLES = 20;  // Muestras para promedio

void setup() {
    Serial.begin(115200);
    
    // Configurar ADC para LM35
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC_CHANNEL_0, ADC_ATTEN_DB_11);
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                            ADC_WIDTH_BIT_12, 1100, &adc_chars);
    
    Serial.println("=== Sensor LM35 Inicializado ===");
    Serial.println("Pin: GPIO36");
    Serial.println("Rango: 0-100°C");
    Serial.println("Precisión: ±0.5°C");
    Serial.println("===============================");
}

void loop() {
    float temperature = readLM35Temperature();
    
    Serial.printf("Temperatura: %.2f°C", temperature);
    
    // Indicador visual de temperatura
    if (temperature < 25) {
        Serial.println(" [FRÍO] ❄️");
    } else if (temperature < 35) {
        Serial.println(" [NORMAL] 🌡️");
    } else {
        Serial.println(" [CALIENTE] 🔥");
    }
    
    delay(1000);
}

float readLM35Temperature() {
    uint32_t adc_sum = 0;
    
    // Promedio de múltiples lecturas
    for (int i = 0; i < SAMPLES; i++) {
        adc_sum += adc1_get_raw(ADC_CHANNEL_0);
        delay(10);
    }
    
    uint32_t adc_average = adc_sum / SAMPLES;
    uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_average, &adc_chars);
    
    // LM35: 10mV/°C
    float temperature = voltage_mv / 10.0;
    
    return temperature;
}
02
Control PWM con Potenciómetro
Intermedio 30-40 min

Objetivo: Controlar brillo de LED o velocidad de motor usando lectura ADC de potenciómetro.

Aplicación: Interfaces de control manual en sistemas automatizados.

Materiales Necesarios
  • ESP32 DevKit
  • Potenciómetro 10kΩ
  • LED de alto brillo + resistor 220Ω
  • Transistor MOSFET (opcional para motor)
Control PWM Avanzado
/**
 * Ejercicio 2: Control PWM con Potenciómetro
 * Mapeo suave de ADC a PWM con histéresis
 */

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

esp_adc_cal_characteristics_t adc_chars;

// Configuración PWM
const int PWM_PIN = 2;
const int PWM_CHANNEL = 0;
const int PWM_FREQUENCY = 5000;  // 5kHz
const int PWM_RESOLUTION = 8;    // 8 bits (0-255)

// Filtrado y mapeo
const int DEAD_ZONE = 50;        // Zona muerta para histéresis
int last_pwm_value = 0;

void setup() {
    Serial.begin(115200);
    
    // Configurar ADC
    adc1_config_width(ADC_WIDTH_BIT_12);
    adc1_config_channel_atten(ADC_CHANNEL_0, ADC_ATTEN_DB_11);
    esp_adc_cal_characterize(ADC_UNIT_1, ADC_ATTEN_DB_11, 
                            ADC_WIDTH_BIT_12, 1100, &adc_chars);
    
    // Configurar PWM
    ledcSetup(PWM_CHANNEL, PWM_FREQUENCY, PWM_RESOLUTION);
    ledcAttachPin(PWM_PIN, PWM_CHANNEL);
    
    Serial.println("=== Control PWM con Potenciómetro ===");
    Serial.println("Potenciómetro: GPIO36");
    Serial.println("Salida PWM: GPIO2");
    Serial.println("Frecuencia: 5kHz, Resolución: 8 bits");
    Serial.println("====================================");
}

void loop() {
    // Lectura promediada del ADC
    uint32_t adc_sum = 0;
    for (int i = 0; i < 10; i++) {
        adc_sum += adc1_get_raw(ADC_CHANNEL_0);
    }
    uint32_t adc_value = adc_sum / 10;
    
    // Mapeo no lineal para mejor control
    int pwm_value = mapWithCurve(adc_value, 0, 4095, 0, 255);
    
    // Aplicar histéresis para evitar fluctuaciones
    if (abs(pwm_value - last_pwm_value) > DEAD_ZONE / 16) {
        last_pwm_value = pwm_value;
        ledcWrite(PWM_CHANNEL, pwm_value);
    }
    
    // Mostrar información
    float percentage = (pwm_value / 255.0) * 100;
    Serial.printf("ADC: %d\tPWM: %d\tPotencia: %.1f%%\n", 
                  adc_value, pwm_value, percentage);
    
    delay(50);
}

int mapWithCurve(int x, int in_min, int in_max, int out_min, int out_max) {
    // Mapeo con curva exponencial para mejor control en rangos bajos
    float normalized = (float)(x - in_min) / (in_max - in_min);
    float curved = normalized * normalized;  // Curva cuadrática
    return out_min + curved * (out_max - out_min);
}
03
Adquisición de Datos Multi-Canal
Avanzado 45-60 min

Objetivo: Sistema de adquisición simultánea de múltiples canales ADC con almacenamiento y análisis estadístico.

Aplicación: Monitoreo multi-variable en procesos industriales complejos.

Materiales Necesarios
  • ESP32 DevKit
  • 3 sensores analógicos diferentes
  • Divisores de voltaje (resistores)
  • Tarjeta microSD (opcional)
Sistema Multi-Canal
/**
 * Ejercicio 3: Adquisición Multi-Canal con Análisis Estadístico
 * Sistema profesional de DAQ (Data Acquisition)
 */

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

esp_adc_cal_characteristics_t adc_chars;

struct ChannelData {
    adc1_channel_t channel;
    String name;
    float calibration_factor;
    String units;
    std::vector samples;
    float min_value;
    float max_value;
    float average;
    float std_deviation;
};

// Configuración de canales
ChannelData channels[] = {
    {ADC1_CHANNEL_0, "Temperatura", 0.1, "°C", {}, 0, 0, 0, 0},
    {ADC1_CHANNEL_3, "Presión", 0.01, "bar", {}, 0, 0, 0, 0},
    {ADC1_CHANNEL_6, "Nivel", 1.0, "%", {}, 0, 0, 0, 0}
};

const int NUM_CHANNELS = sizeof(channels) / sizeof(channels[0]);
const int SAMPLE_SIZE = 100;
unsigned long last_sample = 0;
const unsigned long SAMPLE_INTERVAL = 100;  // 100ms

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 cada canal
    for (int i = 0; i < NUM_CHANNELS; i++) {
        adc1_config_channel_atten(channels[i].channel, ADC_ATTEN_DB_11);
        channels[i].samples.reserve(SAMPLE_SIZE);
    }
    
    Serial.println("=== Sistema de Adquisición Multi-Canal ===");
    Serial.println("Canales configurados: " + String(NUM_CHANNELS));
    Serial.println("Frecuencia de muestreo: 10 Hz");
    Serial.println("Tamaño de buffer: " + String(SAMPLE_SIZE) + " muestras");
    Serial.println("=========================================");
    
    printHeader();
}

void loop() {
    if (millis() - last_sample >= SAMPLE_INTERVAL) {
        // Adquirir datos de todos los canales
        for (int i = 0; i < NUM_CHANNELS; i++) {
            acquireChannelData(i);
        }
        
        // Análisis estadístico cada 10 segundos
        static int sample_count = 0;
        sample_count++;
        
        if (sample_count % 100 == 0) {
            performStatisticalAnalysis();
            printStatistics();
        }
        
        printRealTimeData();
        last_sample = millis();
    }
}

void acquireChannelData(int channel_index) {
    // Lectura promediada para reducir ruido
    uint32_t adc_sum = 0;
    for (int i = 0; i < 5; i++) {
        adc_sum += adc1_get_raw(channels[channel_index].channel);
    }
    uint32_t adc_average = adc_sum / 5;
    
    // Conversión a voltaje y aplicación de calibración
    uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_average, &adc_chars);
    float calibrated_value = (voltage_mv / 1000.0) * channels[channel_index].calibration_factor;
    
    // Almacenar en buffer circular
    if (channels[channel_index].samples.size() >= SAMPLE_SIZE) {
        channels[channel_index].samples.erase(channels[channel_index].samples.begin());
    }
    channels[channel_index].samples.push_back(calibrated_value);
}

void performStatisticalAnalysis() {
    for (int i = 0; i < NUM_CHANNELS; i++) {
        if (channels[i].samples.empty()) continue;
        
        // Calcular estadísticas
        float sum = 0;
        float min_val = channels[i].samples[0];
        float max_val = channels[i].samples[0];
        
        for (float sample : channels[i].samples) {
            sum += sample;
            if (sample < min_val) min_val = sample;
            if (sample > max_val) max_val = sample;
        }
        
        channels[i].average = sum / channels[i].samples.size();
        channels[i].min_value = min_val;
        channels[i].max_value = max_val;
        
        // Calcular desviación estándar
        float variance = 0;
        for (float sample : channels[i].samples) {
            variance += pow(sample - channels[i].average, 2);
        }
        channels[i].std_deviation = sqrt(variance / channels[i].samples.size());
    }
}

void printHeader() {
    Serial.println("\n--- Datos en Tiempo Real ---");
    Serial.print("Timestamp\t");
    for (int i = 0; i < NUM_CHANNELS; i++) {
        Serial.print(channels[i].name + "\t");
    }
    Serial.println();
}

void printRealTimeData() {
    Serial.printf("%lu\t\t", millis() / 1000);
    
    for (int i = 0; i < NUM_CHANNELS; i++) {
        if (!channels[i].samples.empty()) {
            float latest = channels[i].samples.back();
            Serial.printf("%.2f %s\t", latest, channels[i].units.c_str());
        } else {
            Serial.print("N/A\t\t");
        }
    }
    Serial.println();
}

void printStatistics() {
    Serial.println("\n========== ANÁLISIS ESTADÍSTICO ==========");
    for (int i = 0; i < NUM_CHANNELS; i++) {
        Serial.printf("Canal: %s\n", channels[i].name.c_str());
        Serial.printf("  Promedio: %.3f %s\n", channels[i].average, channels[i].units.c_str());
        Serial.printf("  Mínimo: %.3f %s\n", channels[i].min_value, channels[i].units.c_str());
        Serial.printf("  Máximo: %.3f %s\n", channels[i].max_value, channels[i].units.c_str());
        Serial.printf("  Desv. Est: %.3f %s\n", channels[i].std_deviation, channels[i].units.c_str());
        Serial.printf("  Muestras: %d\n", channels[i].samples.size());
        Serial.println("  ----------------------------------------");
    }
    Serial.println("==========================================\n");
}

Proyecto Aplicado: Sistema de Monitoreo Industrial

Control Térmico Automatizado para Proceso Industrial

Desarrollaremos un sistema completo de control térmico que integra múltiples sensores ADC con actuadores para mantener condiciones óptimas de proceso.

Especificaciones del Proyecto
  • Sensores: Temperatura (4 zonas), humedad relativa, presión barométrica
  • Actuadores: Calefactores (PWM), ventiladores (ON/OFF), válvulas (servo)
  • Control: PID automático con setpoints configurables
  • Interfaz: Display OLED + comunicación serial
  • Seguridad: Alarmas por sobretemperatura y fallas de sensor
Lista de Materiales
  • ESP32 DevKit v1
  • 4× Sensor LM35
  • DHT22 (humedad)
  • BMP180 (presión)
  • 2× MOSFET IRF520
  • Servo SG90
  • Display OLED 128×64
  • Relé 5V
  • Fuente 12V/2A
Proyecto Completo - Control Térmico
/**
 * PROYECTO: Sistema de Control Térmico Industrial
 * Autor: Curso ESP32 Mecatrónica UNAM
 * Descripción: Control PID multi-zona con monitoreo continuo
 * Hardware: ESP32 + sensores analógicos + actuadores PWM
 */

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

// ========== CONFIGURACIÓN HARDWARE ==========
esp_adc_cal_characteristics_t adc_chars;

// Pines ADC para sensores de temperatura
const adc1_channel_t TEMP_CHANNELS[] = {
    ADC1_CHANNEL_0,  // GPIO36 - Zona 1
    ADC1_CHANNEL_3,  // GPIO39 - Zona 2  
    ADC1_CHANNEL_6,  // GPIO34 - Zona 3
    ADC1_CHANNEL_7   // GPIO35 - Zona 4
};

// Pines de actuadores
const int HEATER_PINS[] = {2, 4, 16, 17};     // PWM para calefactores
const int FAN_PINS[] = {18, 19};              // Control ON/OFF ventiladores
const int SERVO_PIN = 21;                     // Control válvula principal

// Display OLED
#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64
Adafruit_SSD1306 display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1);

Servo valve_servo;

// ========== VARIABLES DE CONTROL ==========
const int NUM_ZONES = 4;
const int PWM_FREQUENCY = 1000;
const int PWM_RESOLUTION = 8;

struct ThermalZone {
    float current_temp;
    float setpoint;
    float pid_output;
    float error;
    float integral;
    float derivative;
    float last_error;
    bool heater_enabled;
    unsigned long last_update;
};

ThermalZone zones[NUM_ZONES];

// Parámetros PID (ajustables)
const float Kp = 2.0;
const float Ki = 0.1;
const float Kd = 0.5;
const float INTEGRAL_LIMIT = 100.0;

// Configuración del sistema
struct SystemConfig {
    float global_setpoint;
    bool auto_mode;
    bool safety_enabled;
    float max_temp_limit;
    float min_temp_limit;
    int update_interval;
} config = {25.0, true, true, 80.0, 5.0, 1000};

// Variables de estado del sistema
bool system_alarm = false;
String alarm_message = "";
unsigned long last_display_update = 0;
unsigned long last_control_update = 0;

void setup() {
    Serial.begin(115200);
    
    // Inicializar ADC
    initializeADC();
    
    // Configurar actuadores
    initializeActuators();
    
    // Inicializar display
    initializeDisplay();
    
    // Configurar zonas térmicas
    initializeThermalZones();
    
    // Mensaje de inicio
    Serial.println("=== SISTEMA DE CONTROL TÉRMICO INDUSTRIAL ===");
    Serial.println("Zonas: " + String(NUM_ZONES));
    Serial.println("Setpoint inicial: " + String(config.global_setpoint) + "°C");
    Serial.println("Modo: " + String(config.auto_mode ? "AUTOMÁTICO" : "MANUAL"));
    Serial.println("============================================");
    
    displaySystemStatus("SISTEMA", "INICIADO");
    delay(2000);
}

void loop() {
    unsigned long current_time = millis();
    
    // Actualización de control PID
    if (current_time - last_control_update >= config.update_interval) {
        updateTemperatureReadings();
        
        if (config.auto_mode) {
            performPIDControl();
        }
        
        checkSafetyLimits();
        updateActuators();
        
        last_control_update = current_time;
    }
    
    // Actualización de display
    if (current_time - last_display_update >= 500) {
        updateDisplay();
        printSystemStatus();
        last_display_update = current_time;
    }
    
    // Procesar comandos serie
    processSerialCommands();
}

void initializeADC() {
    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);
    
    for (int i = 0; i < NUM_ZONES; i++) {
        adc1_config_channel_atten(TEMP_CHANNELS[i], ADC_ATTEN_DB_11);
    }
}

void initializeActuators() {
    // Configurar PWM para calefactores
    for (int i = 0; i < NUM_ZONES; i++) {
        ledcSetup(i, PWM_FREQUENCY, PWM_RESOLUTION);
        ledcAttachPin(HEATER_PINS[i], i);
        ledcWrite(i, 0);  // Inicializar apagados
    }
    
    // Configurar ventiladores
    for (int i = 0; i < 2; i++) {
        pinMode(FAN_PINS[i], OUTPUT);
        digitalWrite(FAN_PINS[i], LOW);
    }
    
    // Configurar servo
    valve_servo.attach(SERVO_PIN);
    valve_servo.write(0);  // Posición inicial cerrada
}

void initializeDisplay() {
    if (!display.begin(SSD1306_SWITCHCAPVCC, 0x3C)) {
        Serial.println("Error: Display OLED no detectado");
        return;
    }
    
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.display();
}

void initializeThermalZones() {
    for (int i = 0; i < NUM_ZONES; i++) {
        zones[i].setpoint = config.global_setpoint;
        zones[i].heater_enabled = true;
        zones[i].integral = 0;
        zones[i].last_error = 0;
        zones[i].last_update = millis();
    }
}

void updateTemperatureReadings() {
    for (int i = 0; i < NUM_ZONES; i++) {
        // Lectura promediada para reducir ruido
        uint32_t adc_sum = 0;
        for (int j = 0; j < 10; j++) {
            adc_sum += adc1_get_raw(TEMP_CHANNELS[i]);
            delayMicroseconds(100);
        }
        
        uint32_t adc_average = adc_sum / 10;
        uint32_t voltage_mv = esp_adc_cal_raw_to_voltage(adc_average, &adc_chars);
        
        // Conversión LM35: 10mV/°C
        zones[i].current_temp = voltage_mv / 10.0;
        
        // Filtro pasa-bajos simple
        static float filtered_temps[NUM_ZONES] = {0};
        filtered_temps[i] = 0.9 * filtered_temps[i] + 0.1 * zones[i].current_temp;
        zones[i].current_temp = filtered_temps[i];
    }
}

void performPIDControl() {
    unsigned long current_time = millis();
    
    for (int i = 0; i < NUM_ZONES; i++) {
        if (!zones[i].heater_enabled) continue;
        
        float dt = (current_time - zones[i].last_update) / 1000.0;  // Segundos
        
        // Calcular error
        zones[i].error = zones[i].setpoint - zones[i].current_temp;
        
        // Término integral con anti-windup
        zones[i].integral += zones[i].error * dt;
        if (zones[i].integral > INTEGRAL_LIMIT) zones[i].integral = INTEGRAL_LIMIT;
        if (zones[i].integral < -INTEGRAL_LIMIT) zones[i].integral = -INTEGRAL_LIMIT;
        
        // Término derivativo
        zones[i].derivative = (zones[i].error - zones[i].last_error) / dt;
        
        // Salida PID
        zones[i].pid_output = Kp * zones[i].error + 
                             Ki * zones[i].integral + 
                             Kd * zones[i].derivative;
        
        // Limitar salida (0-255 para PWM)
        if (zones[i].pid_output < 0) zones[i].pid_output = 0;
        if (zones[i].pid_output > 255) zones[i].pid_output = 255;
        
        zones[i].last_error = zones[i].error;
        zones[i].last_update = current_time;
    }
}

void checkSafetyLimits() {
    if (!config.safety_enabled) return;
    
    system_alarm = false;
    alarm_message = "";
    
    for (int i = 0; i < NUM_ZONES; i++) {
        if (zones[i].current_temp > config.max_temp_limit) {
            system_alarm = true;
            alarm_message = "SOBRETEMPERATURA Z" + String(i + 1);
            zones[i].heater_enabled = false;
            
            // Activar ventiladores de emergencia
            digitalWrite(FAN_PINS[0], HIGH);
            digitalWrite(FAN_PINS[1], HIGH);
            break;
        }
        
        if (zones[i].current_temp < config.min_temp_limit) {
            system_alarm = true;
            alarm_message = "TEMP BAJA Z" + String(i + 1);
        }
    }
    
    if (system_alarm) {
        Serial.println("ALARMA: " + alarm_message);
    }
}

void updateActuators() {
    for (int i = 0; i < NUM_ZONES; i++) {
        if (system_alarm) {
            // Modo seguridad: apagar calefactores
            ledcWrite(i, 0);
        } else if (zones[i].heater_enabled && config.auto_mode) {
            // Control PID normal
            ledcWrite(i, (int)zones[i].pid_output);
        }
    }
    
    // Control de ventiladores basado en temperatura promedio
    float avg_temp = 0;
    for (int i = 0; i < NUM_ZONES; i++) {
        avg_temp += zones[i].current_temp;
    }
    avg_temp /= NUM_ZONES;
    
    if (avg_temp > config.global_setpoint + 5.0 && !system_alarm) {
        digitalWrite(FAN_PINS[0], HIGH);
        if (avg_temp > config.global_setpoint + 10.0) {
            digitalWrite(FAN_PINS[1], HIGH);
        }
    } else if (!system_alarm) {
        digitalWrite(FAN_PINS[0], LOW);
        digitalWrite(FAN_PINS[1], LOW);
    }
    
    // Control de válvula proporcional
    if (!system_alarm) {
        float valve_angle = map(avg_temp, config.global_setpoint - 10, 
                               config.global_setpoint + 10, 0, 180);
        valve_angle = constrain(valve_angle, 0, 180);
        valve_servo.write(valve_angle);
    }
}

void updateDisplay() {
    display.clearDisplay();
    display.setCursor(0, 0);
    
    // Título
    display.setTextSize(1);
    display.println("CONTROL TERMICO");
    display.println("---------------");
    
    // Estado del sistema
    if (system_alarm) {
        display.println("ALARMA: " + alarm_message);
    } else {
        display.println("Estado: " + String(config.auto_mode ? "AUTO" : "MANUAL"));
    }
    
    // Temperaturas por zona
    display.setTextSize(1);
    for (int i = 0; i < min(NUM_ZONES, 3); i++) {  // Mostrar solo 3 zonas por espacio
        display.printf("Z%d: %.1fC (%.0f%%)\n", 
                      i + 1, zones[i].current_temp, 
                      (zones[i].pid_output / 255.0) * 100);
    }
    
    display.display();
}

void printSystemStatus() {
    Serial.println("\n=== STATUS DEL SISTEMA ===");
    Serial.printf("Tiempo: %lu seg\n", millis() / 1000);
    Serial.printf("Setpoint: %.1f°C\n", config.global_setpoint);
    Serial.printf("Modo: %s\n", config.auto_mode ? "AUTOMÁTICO" : "MANUAL");
    
    for (int i = 0; i < NUM_ZONES; i++) {
        Serial.printf("Zona %d: %.2f°C | Error: %.2f | PID: %.0f | Heater: %s\n",
                     i + 1, zones[i].current_temp, zones[i].error,
                     zones[i].pid_output, zones[i].heater_enabled ? "ON" : "OFF");
    }
    
    if (system_alarm) {
        Serial.println("⚠️  ALARMA ACTIVA: " + alarm_message);
    }
    
    Serial.println("=========================");
}

void processSerialCommands() {
    if (Serial.available()) {
        String command = Serial.readStringUntil('\n');
        command.trim();
        
        if (command.startsWith("SET ")) {
            float new_setpoint = command.substring(4).toFloat();
            if (new_setpoint >= 10 && new_setpoint <= 60) {
                config.global_setpoint = new_setpoint;
                for (int i = 0; i < NUM_ZONES; i++) {
                    zones[i].setpoint = new_setpoint;
                }
                Serial.println("Setpoint actualizado: " + String(new_setpoint) + "°C");
            }
        } else if (command == "AUTO") {
            config.auto_mode = true;
            Serial.println("Modo AUTOMÁTICO activado");
        } else if (command == "MANUAL") {
            config.auto_mode = false;
            Serial.println("Modo MANUAL activado");
        } else if (command == "RESET") {
            // Reset integral terms
            for (int i = 0; i < NUM_ZONES; i++) {
                zones[i].integral = 0;
            }
            system_alarm = false;
            Serial.println("Sistema reiniciado");
        } else if (command == "STATUS") {
            printSystemStatus();
        }
    }
}

void displaySystemStatus(String title, String message) {
    display.clearDisplay();
    display.setCursor(0, 20);
    display.setTextSize(2);
    display.println(title);
    display.setTextSize(1);
    display.println(message);
    display.display();
}
Características Implementadas
  • Control PID multi-zona independiente
  • Filtrado digital avanzado para reducir ruido
  • Sistema de seguridad con límites configurables
  • Interface de comandos serie para configuración
  • Display OLED para monitoreo local
  • Control proporcional de actuadores
  • Gestión de alarmas y estados de emergencia
  • Logging detallado para análisis posterior
Comandos Serie Disponibles
  • SET 25.5 - Establecer setpoint global
  • AUTO - Activar control automático
  • MANUAL - Cambiar a modo manual
  • RESET - Reiniciar sistema y alarmas
  • STATUS - Mostrar estado completo

Referencias Técnicas Especializadas

Papers y Investigación
  • IEEE: "High-Resolution ADC Techniques for IoT Applications"
  • Sensors Journal: "Noise Characterization in Low-Power ADCs"
  • Mechatronics: "Multi-Channel Data Acquisition Systems"