// =============================================================
// Soft Starter Seguro para Compresor AC 110V/60Hz - ATtiny85
// Versión: 2.0 - Con máquina de estados y protecciones
// =============================================================
#include <avr/wdt.h>
#include <avr/interrupt.h>
// ----- Configuración de Pines -----
const uint8_t PIN_TRIAC_GATE = 0; // PB0 -> Pin 5 (MOC3021)
const uint8_t PIN_BYPASS_RELAY = 1; // PB1 -> Pin 6 (Base 2N2222)
const uint8_t PIN_ZERO_CROSS = 2; // PB2 -> Pin 7 (H11AA1, INT0)
const uint8_t PIN_LED_STATUS = 3; // PB3 -> Pin 2 (LED diagnóstico)
const uint8_t PIN_START_CMD = 4; // PB4 -> Pin 3 (Botón/Señal de arranque)
// ----- Máquina de Estados -----
enum State {
STATE_LOCKOUT, // Espera 5 min post-apagado (anti-ciclagio)
STATE_IDLE, // Listo, esperando comando de arranque
STATE_SOFT_START, // Rampa de voltaje activa
STATE_BYPASS_TRANS, // Transición: cierre de relé electromecánico
STATE_RUNNING, // Operación normal (relé cerrado)
STATE_FAULT // Fallo detectado - bloqueo hasta reset
};
volatile State currentState = STATE_LOCKOUT;
// ----- Parámetros de Control de Fase -----
volatile bool zc_flag = false;
int firing_delay_us = 8000; // Retardo inicial (µs)
const int MIN_DELAY_US = 1500; // Retardo mínimo seguro
const int DELAY_STEP = 40; // Reducción por semiciclo
const int PULSE_WIDTH_US = 100; // Ancho de pulso de puerta
// ----- Parámetros de Seguridad -----
const unsigned long LOCKOUT_MS = 60000UL; // 1 a 5 minutos
const unsigned long START_TIMEOUT_MS = 3500UL; // 4 seg máximo de soft-start
const unsigned long ZC_TIMEOUT_MS = 100UL; // 100ms sin ZC = fallo de red
const unsigned long BYPASS_VERIFY_MS = 200UL; // Tiempo para verificar relé
const unsigned long ZC_DEBOUNCE_MS = 2; // Filtro anti-rebotes ZC
// ----- Temporizadores -----
unsigned long stateEntryTime = 0;
unsigned long lastZCtime = 0;
unsigned long lastBlinkTime = 0;
uint16_t zc_counter = 0; // Contador de semiciclos en soft-start
// ----- Flags -----
volatile bool start_requested = false;
bool fault_latched = false;
// =============================================================
// ISR: Detección de Cruce por Cero (con debounce)
// =============================================================
ISR(INT0_vect) {
unsigned long now = millis();
// Filtro de rebotes: ignorar si han pasado menos de 2ms
if ((now - lastZCtime) > ZC_DEBOUNCE_MS) {
zc_flag = true;
lastZCtime = now;
}
}
// =============================================================
// Funciones Auxiliares
// =============================================================
void pulse_triac() {
PORTB |= (1 << PIN_TRIAC_GATE);
delayMicroseconds(PULSE_WIDTH_US);
PORTB &= ~(1 << PIN_TRIAC_GATE);
}
void set_state(State new_state) {
currentState = new_state;
stateEntryTime = millis();
}
void enter_fault(const char* reason) {
currentState = STATE_FAULT;
fault_latched = true;
digitalWrite(PIN_TRIAC_GATE, LOW);
digitalWrite(PIN_BYPASS_RELAY, LOW);
// El LED parpadeará rápido en el loop para indicar fallo
}
// Blink de diagnóstico (no bloqueante)
void status_blink(uint8_t pattern, unsigned long interval) {
if (millis() - lastBlinkTime > interval) {
lastBlinkTime = millis();
digitalWrite(PIN_LED_STATUS, !digitalRead(PIN_LED_STATUS));
}
}
// =============================================================
// SETUP
// =============================================================
void setup() {
// Configuración de pines
pinMode(PIN_TRIAC_GATE, OUTPUT);
pinMode(PIN_BYPASS_RELAY, OUTPUT);
pinMode(PIN_LED_STATUS, OUTPUT);
pinMode(PIN_START_CMD, INPUT_PULLUP); // Activo en LOW
digitalWrite(PIN_TRIAC_GATE, LOW);
digitalWrite(PIN_BYPASS_RELAY, LOW);
digitalWrite(PIN_LED_STATUS, LOW);
// Configuración de Interrupción Externa (INT0 = PB2)
// Usamos registro directo para ATtiny85
GIMSK |= (1 << INT0); // Habilitar INT0
MCUCR |= (1 << ISC01); // Flanco de bajada (FALLING)
MCUCR &= ~(1 << ISC00);
sei(); // Habilitar interrupciones globales
// Iniciar en LOCKOUT (seguridad por defecto al encender)
set_state(STATE_LOCKOUT);
// Activar Watchdog Timer a 2 segundos
wdt_enable(WDTO_2S);
}
// =============================================================
// LOOP PRINCIPAL - Máquina de Estados
// =============================================================
void loop() {
wdt_reset(); // Alimentar el perro guardián
// ----- Lectura de comando de arranque -----
if (digitalRead(PIN_START_CMD) == LOW && !start_requested) {
if (currentState == STATE_IDLE) {
start_requested = true;
}
}
// ----- Detección de fallo de red (ausencia de ZC) -----
if (currentState != STATE_FAULT &&
currentState != STATE_LOCKOUT &&
(millis() - lastZCtime) > ZC_TIMEOUT_MS &&
lastZCtime != 0) {
enter_fault("NO_ZC");
}
// ----- Máquina de Estados -----
switch (currentState) {
// ---------------------------------------------------------
// ESTADO 1: LOCKOUT - Anti-ciclagio de 5 minutos
// ---------------------------------------------------------
case STATE_LOCKOUT:
// Parpadeo lento: 1 blink cada 500ms = "esperando"
status_blink(1, 500);
if ((millis() - stateEntryTime) >= LOCKOUT_MS) {
set_state(STATE_IDLE);
}
break;
// ---------------------------------------------------------
// ESTADO 2: IDLE - Esperando comando de arranque
// ---------------------------------------------------------
case STATE_IDLE:
digitalWrite(PIN_LED_STATUS, HIGH); // LED fijo = listo
if (start_requested) {
start_requested = false;
firing_delay_us = 8000;
zc_counter = 0;
set_state(STATE_SOFT_START);
}
break;
// ---------------------------------------------------------
// ESTADO 3: SOFT START - Rampa de voltaje
// ---------------------------------------------------------
case STATE_SOFT_START:
// Timeout de seguridad: si tarda más de 4s, fallo
if ((millis() - stateEntryTime) > START_TIMEOUT_MS) {
enter_fault("START_TIMEOUT");
break;
}
// Parpadeo medio: 2 blinks/seg = "arrancando"
status_blink(1, 250);
if (zc_flag) {
zc_flag = false;
zc_counter++;
if (firing_delay_us > MIN_DELAY_US) {
delayMicroseconds(firing_delay_us);
pulse_triac();
firing_delay_us -= DELAY_STEP;
} else {
// Rampa completada exitosamente
set_state(STATE_BYPASS_TRANS);
}
}
break;
// ---------------------------------------------------------
// ESTADO 4: BYPASS TRANSITION - Cierre de relé
// ---------------------------------------------------------
case STATE_BYPASS_TRANS:
digitalWrite(PIN_BYPASS_RELAY, HIGH); // Energizar bobina del relé
// Seguir disparando TRIAC en mínimo durante la transición
if (zc_flag) {
zc_flag = false;
delayMicroseconds(MIN_DELAY_US);
pulse_triac();
}
// Esperar 200ms para que el relé cierre físicamente
if ((millis() - stateEntryTime) > BYPASS_VERIFY_MS) {
// Verificación: el relé debería estar cerrado
// (En diseño avanzado se podría leer un contacto auxiliar)
set_state(STATE_RUNNING);
}
break;
// ---------------------------------------------------------
// ESTADO 5: RUNNING - Operación normal
// ---------------------------------------------------------
case STATE_RUNNING:
digitalWrite(PIN_LED_STATUS, LOW); // LED apagado = operando
// Mantener TRIAC disparado como respaldo (corriente fluye por relé)
if (zc_flag) {
zc_flag = false;
delayMicroseconds(MIN_DELAY_US);
pulse_triac();
}
// Detección de comando de paro
if (digitalRead(PIN_START_CMD) == HIGH) { // Soltó el botón / señal off
digitalWrite(PIN_BYPASS_RELAY, LOW);
digitalWrite(PIN_TRIAC_GATE, LOW);
set_state(STATE_LOCKOUT); // Volver a lockout de 5 min
}
break;
// ---------------------------------------------------------
// ESTADO 6: FAULT - Fallo detectado (bloqueo)
// ---------------------------------------------------------
case STATE_FAULT:
digitalWrite(PIN_TRIAC_GATE, LOW);
digitalWrite(PIN_BYPASS_RELAY, LOW);
// Parpadeo rápido: 4 blinks/seg = "FALLO"
status_blink(1, 125);
// Para salir de FAULT: ciclo de energía O mantener botón 3s
if (digitalRead(PIN_START_CMD) == LOW) {
delay(3000); // Anti-rebote largo
if (digitalRead(PIN_START_CMD) == LOW) {
fault_latched = false;
set_state(STATE_LOCKOUT);
}
}
break;
}
}
/*
[ENCENDIDO]
│
▼
─────────────┐
│ LOCKOUT │ ◄── 5 min obligatorios (anti-ciclagio)
│ (5 min) │
└──────┬──────┘
│ (tiempo cumplido)
▼
┌─────────────┐
│ IDLE │ ◄── LED fijo = listo
│ (espera) │
└──────┬──────┘
│ (comando START)
▼
┌─────────────┐
│ SOFT START │ ◄── Rampa 0→100% en ~2s
│ (rampa) │ Timeout 4s → FAULT
└──────┬──────┘
│ (rampa completa)
▼
┌─────────────┐
│ BYPASS │ ◄── Cierre de relé + verificación
│ TRANSITION │ 200ms de espera
└──────┬──────┘
│ (relé cerrado)
▼
┌─────────────┐
│ RUNNING │ ◄── Operación normal
│ (normal) │ Sin ZC 100ms → FAULT
└──────┬──────┘
│ (comando STOP o fallo)
▼
┌─────────────┐
│ FAULT │ ◄── Bloqueo hasta reset manual
│ (bloqueo) │ (botón 3s o ciclo de energía)
└──────┬──────┘
│ (reset)
└──► LOCKOUT
Pin de comando de arranque (PIN_START_CMD): Conéctalo a un pulsador NA con pull-up interno,
o a la salida de un termostato/controlador. Activo en LOW.
LED de diagnóstico (PIN_LED_STATUS):
Fijo encendido = listo para arrancar
Parpadeo lento (500ms) = en lockout
Parpadeo medio (250ms) = en rampa de arranque
Apagado = operando normal
Parpadeo rápido (125ms) = FALLO
Reset de fallo: Mantener presionado el botón de arranque por 3 segundos, o cortar y restaurar la alimentación.
Ajuste del LOCKOUT_MS: Los 5 minutos son estándar en HVAC. Algunos compresores requieren solo 3 min;
ajústalo según la ficha técnica del equipo.
Watchdog Timer: Si el código se cuelga por EMI (muy común cerca de TRIACs),
el ATtiny85 se reinicia automáticamente en 2 segundos y vuelve a LOCKOUT, protegiendo el compresor.
*/
Fijo encendido = listo para arrancar
Parpadeo lento (500ms) = en lockout
Parpadeo medio (250ms) = en rampa de arranque
Apagado = operando normal
Parpadeo rápido (125ms) = FALLO