/**
* @file Inverter_ESP32S3.ino
* @brief Controlador Digital de Inversor de Onda Senoidal Pura con Interfaz Web Offline en Núcleo 0
* @target ESP32-S3 (Waveshare ESP32-S3-Pico o similar)
*
* ARQUITECTURA MULTINÚCLEO XTENSA LX7:
* - NÚCLEO 0: Dedicado exclusivamente a la red inalámbrica offline (Modo Access Point),
* servidor web HTTP, API REST de telemetría y entrega del panel de control HTML5/JS.
* Esto aísla por completo el tráfico TCP/IP y evita interferir con la potencia.
* - NÚCLEO 1 (Bucle Principal): Ejecuta el control de sensores de alta velocidad, regulador PI de voltaje,
* algoritmo de ventilación, alimentación del Watchdog físico y lógica de protección.
* - INTERRUPCIÓN DEL TIMER (IRAM): Modula la SPWM a 20 kHz mediante Síntesis Digital Directa (DDS)
* a 60 Hz en un hilo de ejecución ultra-prioritario por hardware.
*/
#include <Arduino.h>
#include <WiFi.h>
#include <WebServer.h>
#include <esp_task_wdt.h>
#include <math.h>
// ==========================================
// CONFIGURACIÓN DE PINES (ESP32-S3)
// ==========================================
#define PWM_PIN_A 4 // Salida SPWM Rama A (IN1 / HIN)
#define PWM_PIN_B 5 // Salida SPWM Rama B (IN2 / LIN o HIN2)
#define SD_PIN 6 // Pin de Apagado Físico (Shutdown) - Activo ALTO
#define LED_STATUS_PIN 7 // LED de Estado del Sistema
#define FAN_PIN 8 // Pin de Control del Ventilador (ON/OFF o PWM)
// Entradas Analógicas (ADC1)
#define CURRENT_SENSOR_PIN 1 // Corriente de salida CA (ej. ACS712)
#define VOLTAGE_SENSOR_PIN 2 // Voltaje de salida CA (ej. ZMPT101B)
#define TEMP_SENSOR_PIN 3 // Sensor de Temperatura del disipador (LM35 / NTC)
#define INPUT_VOLT_SENSOR_PIN 10 // Voltaje de Entrada CC (Batería / Bus de CD - ADC1_CH9)
// ==========================================
// CONFIGURACIÓN DE LA RED WI-FI (OFFLINE AP)
// ==========================================
const char* AP_SSID = "ESP32S3_Inverter_AP";
const char* AP_PASS = "12345678"; // Mínimo 8 caracteres
// Servidor Web en puerto estándar HTTP
WebServer server(80);
// ==========================================
// CONSTANTES FÍSICAS Y LÍMITES
// ==========================================
const float TARGET_V_RMS = 120.0; // Tensión CA objetivo (V RMS)
const float MAX_CURRENT_LIMIT = 15.0; // Límite de corriente pico (Amperios)
const float MAX_VOLTAGE_LIMIT = 140.0; // Límite de sobretensión (V RMS)
const float OVERTEMP_LIMIT = 75.0; // Límite de apagado por temperatura (°C)
const float FAN_ON_TEMP = 40.0; // Temperatura para encender ventilador (°C)
const float FAN_OFF_TEMP = 35.0; // Temperatura para apagar ventilador (°C)
// ==========================================
// PARÁMETROS SPWM Y DDS (60 Hz)
// ==========================================
const uint32_t CARRIER_FREQ = 20000; // Frecuencia SPWM (20 kHz, inaudible)
const uint8_t PWM_RESOLUTION = 10; // 10 bits de resolución (0-1023)
const uint32_t MAX_DUTY = 1023;
const uint32_t ISR_FREQ = 20000; // Interrupción cada 50 microsegundos
const float TARGET_FREQ = 60.0; // Frecuencia objetivo de salida (Hz)
// Incremento de fase DDS: Delta = (f_out * 2^32) / f_timer
const uint32_t PHASE_INCREMENT = (uint32_t)((TARGET_FREQ * 4294967296.0) / (float)ISR_FREQ);
uint16_t sine_table[256];
// ==========================================
// VARIABLES COMPARTIDAS (VOLÁTILES / ATÓMICAS)
// ==========================================
volatile uint32_t phase_accumulator = 0;
volatile bool system_enabled = false;
volatile uint32_t isr_heartbeat = 0;
enum SystemState {
STATE_INIT,
STATE_NORMAL,
STATE_FAULT
};
volatile SystemState current_state = STATE_INIT;
enum FaultType {
FAULT_NONE,
FAULT_OVERCURRENT,
FAULT_OVERVOLTAGE,
FAULT_OVERTEMP,
FAULT_ISR_TIMEOUT
};
volatile FaultType active_fault = FAULT_NONE;
// Mediciones y Control (Modificadas en Núcleo 1, leídas en Núcleo 0)
volatile float actual_v_rms = 0.0;
volatile float actual_v_input = 0.0; // Tensión de entrada CC
volatile float actual_current = 0.0;
volatile float actual_temp = 25.0;
volatile uint16_t modulation_index = 800; // Regulado de 100 a 1000
// Regulador PI
float pi_integral = 0.0;
const float Kp = 1.0;
const float Ki = 0.04;
const float dt = 0.05; // Período del lazo (50 ms)
#define LEDC_CHAN_A 0
#define LEDC_CHAN_B 1
// ==========================================
// COMPATIBILIDAD ARDUINO CORE V2 / V3
// ==========================================
#if defined(ESP_ARDUINO_VERSION_MAJOR) && ESP_ARDUINO_VERSION_MAJOR >= 3
#define USE_CORE_V3
hw_timer_t *timer = NULL;
#else
#define USE_CORE_V2
hw_timer_t *timer = NULL;
#endif
void initPWM(uint8_t pin, uint32_t freq, uint8_t resolution, uint8_t channel) {
#if defined(USE_CORE_V3)
ledcAttach(pin, freq, resolution);
#else
ledcSetup(channel, freq, resolution);
ledcAttachPin(pin, channel);
#endif
}
static inline void writePWM(uint8_t pin, uint8_t channel, uint32_t duty) {
#if defined(USE_CORE_V3)
ledcWrite(pin, duty);
#else
ledcWrite(channel, duty);
#endif
}
// ==========================================
// RUTINA DE INTERRUPCIÓN (SPWM DDS) en IRAM
// ==========================================
void ARDUINO_ISR_ATTR onTimer() {
isr_heartbeat++;
if (!system_enabled) {
writePWM(PWM_PIN_A, LEDC_CHAN_A, 0);
writePWM(PWM_PIN_B, LEDC_CHAN_B, 0);
return;
}
// Acumulación de fase DDS
phase_accumulator += PHASE_INCREMENT;
uint8_t index = phase_accumulator >> 24;
// Modulación unipolar calculada mediante enteros
uint32_t raw_duty = (sine_table[index] * modulation_index) >> 10;
if (raw_duty > MAX_DUTY) raw_duty = MAX_DUTY;
if (index < 128) {
writePWM(PWM_PIN_A, LEDC_CHAN_A, raw_duty);
writePWM(PWM_PIN_B, LEDC_CHAN_B, 0);
} else {
writePWM(PWM_PIN_A, LEDC_CHAN_A, 0);
writePWM(PWM_PIN_B, LEDC_CHAN_B, raw_duty);
}
}
// ==========================================
// MÉTODOS DE CONVERSIÓN DE TEXTO PARA TELEMETRÍA
// ==========================================
const char* getStateString() {
switch (current_state) {
case STATE_INIT: return "INICIANDO";
case STATE_NORMAL: return "NORMAL (OK)";
case STATE_FAULT: return "FALLA (BLOQUEADO)";
default: return "DESCONOCIDO";
}
}
const char* getFaultString() {
switch (active_fault) {
case FAULT_NONE: return "Ninguna";
case FAULT_OVERCURRENT: return "SOBRECORRIENTE DETECTADA (OCP)";
case FAULT_OVERVOLTAGE: return "SOBRETENSIÓN EN SALIDA (OVP)";
case FAULT_OVERTEMP: return "SOBRETEMPERATURA EN DISIPADOR (OTP)";
case FAULT_ISR_TIMEOUT: return "ERROR DE EJECUCIÓN (TIMEOUT ISR)";
default: return "DESCONOCIDA";
}
}
// ==========================================
// PLANTILLA HTML5 / CSS DEL PANEL DE CONTROL (Web Server)
// ==========================================
const char HTML_INDEX[] PROGMEM = R"rawhtml(
<!DOCTYPE html>
<html lang="es">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Inversor ESP32-S3 - Panel de Telemetría</title>
<style>
:root {
--bg-color: #0f172a;
--card-bg: #1e293b;
--text-color: #f8fafc;
--primary: #10b981;
--accent: #06b6d4;
--danger: #ef4444;
--warning: #f59e0b;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
background-color: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
}
.container {
max-width: 900px;
width: 100%;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 2px solid #334155;
padding-bottom: 15px;
margin-bottom: 25px;
}
h1 { margin: 0; font-size: 1.8rem; color: var(--accent); }
.status-badge {
padding: 8px 16px;
border-radius: 20px;
font-weight: bold;
text-transform: uppercase;
font-size: 0.9rem;
letter-spacing: 0.5px;
}
.state-normal { background-color: rgba(16, 185, 129, 0.2); color: var(--primary); border: 1px solid var(--primary); }
.state-fault { background-color: rgba(239, 68, 68, 0.2); color: var(--danger); border: 1px solid var(--danger); animation: pulse 1.5s infinite; }
.state-init { background-color: rgba(245, 158, 11, 0.2); color: var(--warning); border: 1px solid var(--warning); }
@keyframes pulse {
0% { opacity: 0.7; }
50% { opacity: 1; }
100% { opacity: 0.7; }
}
.fault-banner {
background-color: rgba(239, 68, 68, 0.15);
border: 2px solid var(--danger);
border-radius: 10px;
padding: 15px;
margin-bottom: 25px;
display: none;
align-items: center;
justify-content: space-between;
}
.fault-banner h3 { margin: 0; color: var(--danger); }
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
gap: 20px;
margin-bottom: 25px;
}
.card {
background-color: var(--card-bg);
border-radius: 12px;
padding: 20px;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
border: 1px solid #334155;
position: relative;
overflow: hidden;
}
.card::before {
content: '';
position: absolute;
top: 0; left: 0; width: 4px; height: 100%;
background-color: var(--accent);
}
.card.output-v::before { background-color: #3b82f6; }
.card.input-v::before { background-color: var(--accent); }
.card.power::before { background-color: #8b5cf6; }
.card.temp::before { background-color: var(--warning); }
.card-title {
font-size: 0.85rem;
text-transform: uppercase;
color: #94a3b8;
margin-bottom: 10px;
font-weight: 600;
letter-spacing: 0.5px;
}
.card-value {
font-size: 2.2rem;
font-weight: 700;
margin: 0;
display: flex;
align-items: baseline;
}
.card-unit {
font-size: 1.1rem;
color: #94a3b8;
margin-left: 5px;
font-weight: 500;
}
.system-info {
background-color: var(--card-bg);
border-radius: 12px;
padding: 20px;
border: 1px solid #334155;
display: flex;
flex-wrap: wrap;
justify-content: space-between;
gap: 15px;
}
.info-item {
display: flex;
flex-direction: column;
min-width: 150px;
}
.info-label { font-size: 0.85rem; color: #94a3b8; margin-bottom: 5px; }
.info-val { font-size: 1.1rem; font-weight: 600; }
.btn {
background-color: var(--danger);
color: white;
border: none;
padding: 10px 20px;
border-radius: 8px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s;
}
.btn:hover { background-color: #dc2626; }
footer {
text-align: center;
margin-top: 40px;
font-size: 0.8rem;
color: #64748b;
}
</style>
</head>
<body>
<div class="container">
<header>
<div>
<h1>ESP32-S3 Inverter OS</h1>
<div style="font-size: 0.85rem; color: #64748b; margin-top: 4px;">WiFi Offline Control Dashboard</div>
</div>
<div id="statusBadge" class="status-badge state-init">Iniciando</div>
</header>
<div id="faultBanner" class="fault-banner">
<div>
<h3>ALERTA DE SEGURIDAD</h3>
<p id="faultDesc" style="margin: 5px 0 0 0; font-size: 0.95rem;"></p>
</div>
<button class="btn" onclick="resetInverter()">Reiniciar Inversor</button>
</div>
<div class="grid">
<div class="card output-v">
<div class="card-title">Tensión de Salida</div>
<p class="card-value" id="valOutV">0.0<span class="card-unit">VAC</span></p>
</div>
<div class="card input-v">
<div class="card-title">Tensión de Entrada</div>
<p class="card-value" id="valInV">0.0<span class="card-unit">VCC</span></p>
</div>
<div class="card">
<div class="card-title">Corriente de Carga</div>
<p class="card-value" id="valCurrent">0.00<span class="card-unit">A</span></p>
</div>
<div class="card power">
<div class="card-title">Potencia Activa</div>
<p class="card-value" id="valPower">0<span class="card-unit">W</span></p>
</div>
<div class="card temp">
<div class="card-title">Temperatura Disipador</div>
<p class="card-value" id="valTemp">0.0<span class="card-unit">°C</span></p>
</div>
<div class="card">
<div class="card-title">Estado Ventilador</div>
<p class="card-value" id="valFan" style="font-size: 1.8rem; color: var(--primary);">APAGADO</p>
</div>
</div>
<div class="system-info">
<div class="info-item">
<span class="info-label">Frecuencia de Salida</span>
<span class="info-val" id="valFreq">60.0 Hz</span>
</div>
<div class="info-item">
<span class="info-label">Frecuencia Portadora</span>
<span class="info-val">20.0 kHz (SPWM)</span>
</div>
<div class="info-item">
<span class="info-label">Índice de Modulación</span>
<span class="info-val" id="valMod">0.0%</span>
</div>
<div class="info-item">
<span class="info-label">Asignación de Núcleos</span>
<span class="info-val" style="color: var(--accent);">Núcleo 0: Web / Núcleo 1: Potencia</span>
</div>
</div>
<footer>
Monitoreo en Tiempo Real - ESP32-S3 Pico Hardware
</footer>
</div>
<script>
function updateTelemetry() {
fetch('/data')
.then(response => response.json())
.then(data => {
// Actualizar métricas principales
document.getElementById('valOutV').innerHTML = data.out_v.toFixed(1) + '<span class="card-unit">VAC</span>';
document.getElementById('valInV').innerHTML = data.in_v.toFixed(1) + '<span class="card-unit">VCC</span>';
document.getElementById('valCurrent').innerHTML = data.curr.toFixed(2) + '<span class="card-unit">A</span>';
document.getElementById('valPower').innerHTML = Math.round(data.power) + '<span class="card-unit">W</span>';
document.getElementById('valTemp').innerHTML = data.temp.toFixed(1) + '<span class="card-unit">°C</span>';
document.getElementById('valFreq').innerText = data.freq.toFixed(1) + ' Hz';
document.getElementById('valMod').innerText = ((data.mod / 1024) * 100).toFixed(1) + '%';
// Ventilador
const fanEl = document.getElementById('valFan');
if (data.fan) {
fanEl.innerText = 'ENCENDIDO';
fanEl.style.color = '#10b981';
} else {
fanEl.innerText = 'APAGADO';
fanEl.style.color = '#64748b';
}
// Estado del sistema y alarma
const badge = document.getElementById('statusBadge');
const banner = document.getElementById('faultBanner');
const desc = document.getElementById('faultDesc');
badge.innerText = data.state;
if (data.state === 'FALLA (BLOQUEADO)') {
badge.className = 'status-badge state-fault';
banner.style.display = 'flex';
desc.innerText = data.fault;
} else if (data.state === 'NORMAL (OK)') {
badge.className = 'status-badge state-normal';
banner.style.display = 'none';
} else {
badge.className = 'status-badge state-init';
banner.style.display = 'none';
}
})
.catch(err => console.error('Error de conexión con el inversor:', err));
}
function resetInverter() {
if (confirm('¿Está seguro de reiniciar físicamente el microcontrolador del inversor?')) {
fetch('/reset')
.then(r => alert('Comando de reinicio enviado. El inversor se reestablecerá ahora.'))
.catch(e => alert('Error enviando reinicio. Intente nuevamente.'));
}
}
// Actualización en tiempo real cada 800 ms
setInterval(updateTelemetry, 800);
updateTelemetry();
</script>
</body>
</html>
)rawhtml";
// ==========================================
// CONTROLADORES DE RUTA DEL SERVIDOR WEB
// ==========================================
void handleRoot() {
server.send_P(200, "text/html", HTML_INDEX);
}
void handleGetData() {
// Construir una respuesta JSON compacta con lecturas atómicas
char json[300];
snprintf(json, sizeof(json),
"{\"out_v\":%.1f,\"in_v\":%.1f,\"curr\":%.2f,\"power\":%.1f,\"temp\":%.1f,\"fan\":%s,\"freq\":%.1f,\"mod\":%d,\"state\":\"%s\",\"fault\":\"%s\"}",
actual_v_rms,
actual_v_input,
actual_current,
(actual_v_rms * actual_current),
actual_temp,
(digitalRead(FAN_PIN) == HIGH) ? "true" : "false",
TARGET_FREQ,
modulation_index,
getStateString(),
getFaultString()
);
server.send(200, "application/json", json);
}
void handleReset() {
server.send(200, "text/plain", "Reinicio iniciado...");
delay(500);
ESP.restart();
}
// ==========================================
// TAREA DEDICADA DEL NÚCLEO 0 (WIFI Y WEB)
// ==========================================
TaskHandle_t WebTaskHandle = NULL;
void WebServerTask(void *pvParameters) {
Serial.print("[Core 0] Iniciando Red WiFi y Servidor Web... Hilo ejecutándose en Core: ");
Serial.println(xPortGetCoreID());
// Configurar ESP32-S3 como Punto de Acceso (AP) sin requerir internet externo
WiFi.mode(WIFI_AP);
WiFi.softAPConfig(
IPAddress(192, 168, 4, 1), // IP de la interfaz web (http://192.168.4.1)
IPAddress(192, 168, 4, 1), // Gateway
IPAddress(255, 255, 255, 0) // Máscara de subred
);
WiFi.softAP(AP_SSID, AP_PASS);
Serial.print("[Core 0] WiFi AP Listo. SSID: ");
Serial.print(AP_SSID);
Serial.print(" | IP: ");
Serial.println(WiFi.softAPIP());
// Registrar rutas REST y HTTP
server.on("/", HTTP_GET, handleRoot);
server.on("/data", HTTP_GET, handleGetData);
server.on("/reset", HTTP_GET, handleReset);
server.begin();
Serial.println("[Core 0] Servidor Web HTTP inicializado exitosamente.");
// Lazo continuo del Core 0
while (true) {
server.handleClient();
vTaskDelay(pdMS_TO_TICKS(15)); // Ceder tiempo de CPU para tareas de red internas (LwIP)
}
}
// ==========================================
// PROCEDIMIENTO DE APAGADO DE EMERGENCIA (FAST)
// ==========================================
void triggerFault(FaultType fault) {
system_enabled = false;
digitalWrite(SD_PIN, HIGH); // Apagado por hardware inmediato (Gate Driver Shutdown)
writePWM(PWM_PIN_A, LEDC_CHAN_A, 0);
writePWM(PWM_PIN_B, LEDC_CHAN_B, 0);
active_fault = fault;
current_state = STATE_FAULT;
digitalWrite(LED_STATUS_PIN, HIGH); // LED fijo indica falla
// Imprimir alerta crítica en terminal
Serial.print("\n!!! APAGADO POR SEGURIDAD !!! MOTIVO: ");
switch (fault) {
case FAULT_OVERCURRENT: Serial.println("SOBRECORRIENTE"); break;
case FAULT_OVERVOLTAGE: Serial.println("SOBRETENSIÓN CA"); break;
case FAULT_OVERTEMP: Serial.println("SOBRETEMPERATURA"); break;
case FAULT_ISR_TIMEOUT: Serial.println("BLOQUEO DE INTERRUPCIÓN (TIMEOUT)"); break;
default: Serial.println("DESCONOCIDO"); break;
}
}
// ==========================================
// PREPARACIÓN DE TABLA DE SENO RECTIFICADA
// ==========================================
void initSineTable() {
for (int i = 0; i < 256; i++) {
float angle = (i * 2.0 * M_PI) / 256.0;
sine_table[i] = (uint16_t)(fabs(sin(angle)) * (float)MAX_DUTY + 0.5);
}
}
// ==========================================
// CONFIGURACIÓN GLOBAL (SETUP)
// ==========================================
void setup() {
Serial.begin(115200);
delay(1000);
Serial.println("\n=======================================================");
Serial.println("=== SISTEMA OPERATIVO INVERSOR DUAL-CORE ESP32-S3 ===");
Serial.println("=======================================================");
// Configuración de Pines
pinMode(SD_PIN, OUTPUT);
digitalWrite(SD_PIN, HIGH); // Apagado por hardware por defecto en el inicio
pinMode(LED_STATUS_PIN, OUTPUT);
digitalWrite(LED_STATUS_PIN, HIGH); // LED encendido fija que está inicializando
pinMode(FAN_PIN, OUTPUT);
digitalWrite(FAN_PIN, LOW);
// Inicializar periféricos analógicos
analogReadResolution(12);
// Preparar tabla de onda senoidal
initSineTable();
// Configuración de salidas PWM (LEDC)
initPWM(PWM_PIN_A, CARRIER_FREQ, PWM_RESOLUTION, LEDC_CHAN_A);
initPWM(PWM_PIN_B, CARRIER_FREQ, PWM_RESOLUTION, LEDC_CHAN_B);
writePWM(PWM_PIN_A, LEDC_CHAN_A, 0);
writePWM(PWM_PIN_B, LEDC_CHAN_B, 0);
// CONFIGURACIÓN DEL HARDWARE WATCHDOG TIMER (WDT)
// Monitorea el bucle del Core 1 para evitar congelamientos en la etapa de potencia
Serial.println("Inicializando Hardware Watchdog...");
#if defined(USE_CORE_V3)
esp_task_wdt_config_t wdt_config = {
.timeout_ms = 4000, // 4 segundos de tiempo límite
.idle_core_mask = (1 << 0) | (1 << 1), // Monitorear ambos núcleos
.trigger_panic = true // Reiniciar si se interrumpe
};
esp_task_wdt_init(&wdt_config);
esp_task_wdt_add(NULL); // Vincular tarea principal de Core 1
#else
esp_task_wdt_init(4, true); // 4 segundos, pánico activo
esp_task_wdt_add(NULL);
#endif
// INICIAR TIMER DE HARDWARE PARA DDS (Asegura SPWM a 20 kHz constante)
#if defined(USE_CORE_V3)
timer = timerBegin(1000000); // Base de tiempo de 1 uS (1 MHz)
if (timer) {
timerAttachInterrupt(timer, &onTimer);
timerAlarm(timer, 50, true, 0); // Disparar cada 50 cuentas (50 uS = 20 kHz)
}
#else
timer = timerBegin(0, 80, true); // Reloj a 1 MHz
if (timer) {
timerAttachInterrupt(timer, &onTimer, true);
timerAlarmWrite(timer, 50, true);
timerAlarmEnable(timer);
}
#endif
if (timer == NULL) {
Serial.println("[ERROR CRÍTICO] No se pudo inicializar el temporizador de hardware.");
while (1);
}
Serial.println("[OK] Temporizador de Hardware SPWM configurado a 20 kHz.");
// CREAR TAREA DEL SERVIDOR WEB EN EL NÚCLEO 0
// Pinned to Core 0 de forma explícita
xTaskCreatePinnedToCore(
WebServerTask, // Puntero a la función de la tarea
"WebServerTask", // Nombre de la tarea
8192, // Tamaño de pila (8 KB)
NULL, // Parámetros de la tarea
1, // Prioridad (Suficiente para operaciones de red)
&WebTaskHandle, // Manejador
0 // Ejecutada de forma exclusiva en el NÚCLEO 0
);
// Retardo de estabilidad antes de encender el Puente H
delay(1500);
digitalWrite(SD_PIN, LOW); // Habilitar controladores físicos de compuerta
system_enabled = true;
current_state = STATE_NORMAL;
digitalWrite(LED_STATUS_PIN, LOW); // Apagar indicador de inicialización
Serial.println(">>> ETAPA DE POTENCIA ACTIVA Y SEPARACIÓN DE NÚCLEOS OK <<<\n");
}
// ==========================================
// ADQUISICIÓN Y FILTRADO DE DATOS (NÚCLEO 1)
// ==========================================
void readSensors() {
// 1. Filtrado de Corriente de Salida CA (ACS712)
int current_raw = analogRead(CURRENT_SENSOR_PIN);
float volt_curr = (current_raw / 4095.0) * 3.3;
float current_sample = (volt_curr - 1.65) / 0.100; // Sensibilidad de 100 mV/A
actual_current = (0.85 * actual_current) + (0.15 * abs(current_sample));
// 2. Tensión de Salida CA RMS (ZMPT101B)
static uint32_t sum_squares = 0;
static uint16_t sample_count = 0;
int volt_raw = analogRead(VOLTAGE_SENSOR_PIN);
int volt_ac = volt_raw - 2048; // Centrado en Vcc/2
sum_squares += (volt_ac * volt_ac);
sample_count++;
if (sample_count >= 20) {
float mean_square = (float)sum_squares / sample_count;
const float VOLT_CALIBRATION = 0.082; // Ajustable con voltímetro real
actual_v_rms = sqrt(mean_square) * VOLT_CALIBRATION;
sum_squares = 0;
sample_count = 0;
}
// 3. Tensión de Entrada CC (Batería / Bus de CD)
int in_raw = analogRead(INPUT_VOLT_SENSOR_PIN);
// Divisor resistivo externo (ej. factor 1:10 para medir hasta 33V CC con seguridad)
float volt_in_pin = (in_raw / 4095.0) * 3.3;
const float DIVIDER_FACTOR = 11.0; // Escalado del divisor de tensión físico
actual_v_input = (0.9 * actual_v_input) + (0.1 * (volt_in_pin * DIVIDER_FACTOR));
// 4. Temperatura del Disipador
int temp_raw = analogRead(TEMP_SENSOR_PIN);
float temp_volt = (temp_raw / 4095.0) * 3.3;
float temp_sample = temp_volt * 100.0; // Lineal de ejemplo (LM35)
actual_temp = (0.95 * actual_temp) + (0.05 * temp_sample);
}
// ==========================================
// REGULACIÓN DE VOLTAJE PI (NÚCLEO 1)
// ==========================================
void regulateVoltage() {
if (current_state != STATE_NORMAL) return;
float error = TARGET_V_RMS - actual_v_rms;
pi_integral += error * dt;
pi_integral = constrain(pi_integral, -150.0, 150.0); // Anti-windup
float output = (Kp * error) + (Ki * pi_integral);
int32_t next_mod = 800 + (int32_t)output;
modulation_index = constrain(next_mod, 100, 1000);
}
// ==========================================
// VENTILACIÓN CON HISTÉRESIS (NÚCLEO 1)
// ==========================================
void handleCooling() {
if (actual_temp >= FAN_ON_TEMP) {
digitalWrite(FAN_PIN, HIGH);
} else if (actual_temp <= FAN_OFF_TEMP) {
digitalWrite(FAN_PIN, LOW);
}
}
// ==========================================
// DIAGNÓSTICOS DE SEGURIDAD (NÚCLEO 1)
// ==========================================
void checkSafeties() {
if (current_state == STATE_FAULT) return;
// 1. Protección de Sobrecorriente Instantánea (OCP)
if (actual_current > MAX_CURRENT_LIMIT) {
triggerFault(FAULT_OVERCURRENT);
return;
}
// 2. Protección de Sobretensión CA de Salida (OVP)
if (actual_v_rms > MAX_VOLTAGE_LIMIT) {
triggerFault(FAULT_OVERVOLTAGE);
return;
}
// 3. Protección de Sobretemperatura (OTP)
if (actual_temp > OVERTEMP_LIMIT) {
triggerFault(FAULT_OVERTEMP);
return;
}
// 4. Antibloqueo del Timer (Watchdog del Latido de la ISR)
// Si la interrupción deja de responder (por hardware o pánico), este hilo apaga todo
static uint32_t last_heartbeat = 0;
static uint32_t last_heartbeat_check = 0;
uint32_t current_time = millis();
if (current_time - last_heartbeat_check >= 400) {
last_heartbeat_check = current_time;
if (isr_heartbeat == last_heartbeat) {
triggerFault(FAULT_ISR_TIMEOUT);
return;
}
last_heartbeat = isr_heartbeat;
}
}
// ==========================================
// BUCLE DE EJECUCIÓN PRINCIPAL (NÚCLEO 1)
// ==========================================
void loop() {
// Alimentar el Watchdog de hardware del Core 1
esp_task_wdt_reset();
static uint32_t last_10ms = 0;
static uint32_t last_50ms = 0;
static uint32_t last_1000ms = 0;
uint32_t now = millis();
// Tareas de alta velocidad de sensado y protección (10 ms)
if (now - last_10ms >= 10) {
last_10ms = now;
readSensors();
checkSafeties();
}
// Lazo PI de control de tensión y ventilación (50 ms)
if (now - last_50ms >= 50) {
last_50ms = now;
regulateVoltage();
handleCooling();
}
// Latido visual y telemetría por consola local (1000 ms)
if (now - last_1000ms >= 1000) {
last_1000ms = now;
if (current_state == STATE_NORMAL) {
digitalWrite(LED_STATUS_PIN, !digitalRead(LED_STATUS_PIN));
}
// Telemetría de diagnóstico local (Opcional, muy útil para debug)
Serial.printf("[Core 1] V_OUT: %.1f V | V_IN: %.1f V | I: %.2f A | P: %.1f W | Temp: %.1f C | Mod: %.1f%%\n",
actual_v_rms, actual_v_input, actual_current, (actual_v_rms * actual_current),
actual_temp, ((float)modulation_index/1024.0)*100.0);
}
}Loading
xiao-esp32-s3
xiao-esp32-s3