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ón | 12 bits (4096 niveles) |
Número de canales | 18 canales (ADC1: 8, ADC2: 10) |
Velocidad de muestreo | Hasta 2 MSPS |
Rango de voltaje | 0V - 3.3V (configurable) |
Precisión típica | ±2 LSB |
Atenuación | 0dB, 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_CH0GPIO37
- ADC1_CH1GPIO38
- ADC1_CH2GPIO39
- ADC1_CH3GPIO32
- ADC1_CH4GPIO33
- ADC1_CH5GPIO34
- ADC1_CH6GPIO35
- ADC1_CH7Configuración de Atenuación
Atenuación | Rango Voltaje |
---|---|
0dB | 0V - 1.1V |
2.5dB | 0V - 1.5V |
6dB | 0V - 2.2V |
11dB | 0V - 3.9V |
Implementación Práctica
Configuración Básica del ADC
/**
* 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
/**
* 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
/**
* 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
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)
/**
* 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;
}
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)
/**
* 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);
}
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)
/**
* 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: 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 globalAUTO
- Activar control automáticoMANUAL
- Cambiar a modo manualRESET
- Reiniciar sistema y alarmasSTATUS
- Mostrar estado completo
Referencias Técnicas Especializadas
Documentación Oficial
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"