/* * GIOSTRINA ESPOSITIVA - firmware di controllo * Scheda: ESP32 DevKit (ESP32-WROOM-32), core Arduino-ESP32 2.0.x o 3.x * * Funzioni: * - rotazione lenta con rampa dolce di avvio e arresto (motore passo-passo, driver STEP/DIR) * - velocita' regolabile da potenziometro (60-90 s per giro) * - accensione e spegnimento a orario (RTC DS3231, con sincronizzazione NTP opzionale) * - modalita' "vetrina": ogni N giri si ferma con il prodotto di punta verso il vetro * - LED 24 V dimmerabili con dissolvenza * - rilevamento di blocco: se il sensore di giro non vede passare il magnete, ferma tutto * * STATO: NON ancora compilato ne' provato su scheda (vedi elettronica/schema.md, "Da verificare"). * * Caricamento: vedi elettronica/schema.md, sezione "Caricare il firmware". */ #include #include #include #include #include #include "esp_timer.h" // ============================================================================ // PARAMETRI CONFIGURABILI // ============================================================================ // --- meccanica (devono corrispondere a cad/params.py) ----------------------- const float RAPPORTO = 8.25f; // R_PISTA / raggio ruota = 330 / 40 const int PASSI_GIRO_MOTORE = 200; // motore 1,8 gradi const int MICROPASSI = 16; // impostati sul driver (MS1/MS2) const bool SENSO_ORARIO = true; // verso di rotazione visto dall'alto // --- velocita' ------------------------------------------------------------------ const float PERIODO_MIN_S = 60.0f; // potenziometro tutto a destra const float PERIODO_MAX_S = 90.0f; // potenziometro tutto a sinistra const float PERIODO_DEFAULT_S = 75.0f; // usato se USA_POTENZIOMETRO = false const bool USA_POTENZIOMETRO = true; const float TEMPO_RAMPA_S = 4.0f; // durata di avvio e arresto // --- orari (ora locale italiana, con ora legale automatica) --------------------- const int ORA_ACCENSIONE = 9; // 09:00 const int MIN_ACCENSIONE = 0; const int ORA_SPEGNIMENTO = 21; // 21:00 const int MIN_SPEGNIMENTO = 0; // giorni attivi: domenica, lunedi', ..., sabato const bool GIORNI_ATTIVI[7] = { true, true, true, true, true, true, true }; // --- luci ----------------------------------------------------------------------- const int LED_GIORNO = 80; // % in orario di apertura const int LED_NOTTE = 0; // % fuori orario (es. 10 per una luce di cortesia) const float DISSOLVENZA_S = 3.0f; // --- modalita' vetrina ------------------------------------------------------------ const bool MODO_VETRINA = true; const int GIRI_TRA_LE_PAUSE = 3; // una pausa ogni 3 giri const float DURATA_PAUSA_S = 12.0f; // angolo, misurato nel verso di rotazione, tra il passaggio del magnete sul sensore // e la posizione in cui il prodotto di punta guarda il vetro (da tarare al montaggio) const float ANGOLO_PUNTA_GRADI = 90.0f; // --- sicurezza ---------------------------------------------------------------------- const float TOLLERANZA_GIRO = 1.5f; // blocco se il giro dura piu' di 1,5 volte il previsto const int TENTATIVI_RIAVVIO = 3; const float ATTESA_RIAVVIO_S = 10.0f; // --- rete (opzionale: lasciare SSID vuoto per usare solo l'orologio DS3231) --------- const char* WIFI_SSID = ""; const char* WIFI_PASSWORD = ""; const char* SERVER_NTP = "pool.ntp.org"; const char* FUSO_ORARIO = "CET-1CEST,M3.5.0,M10.5.0/3"; // --- piedini ---------------------------------------------------------------------------- const int PIN_STEP = 25; const int PIN_DIR = 26; const int PIN_ENABLE = 27; // attivo basso sui driver TMC const int PIN_LED = 32; // gate del MOSFET dei LED (PWM) const int PIN_HALL = 34; // sensore di giro, attivo basso, pull-up esterno 10k a 3,3 V const int PIN_TASTO = 33; // pulsante verso massa const int PIN_POT = 35; // cursore del potenziometro 10k const int PIN_SPIA = 2; // LED sulla scheda const int PIN_SDA = 21; const int PIN_SCL = 22; // ============================================================================ // STATO // ============================================================================ enum Stato { FERMO, AVVIO, MARCIA, ARRESTO, PAUSA_VETRINA, ATTESA_RIAVVIO, GUASTO }; Stato stato = FERMO; const float PASSI_GIRO_PIATTAFORMA = PASSI_GIRO_MOTORE * MICROPASSI * RAPPORTO; // 26400 esp_timer_handle_t timerPassi = NULL; volatile bool livelloStep = false; volatile int32_t passiDalMagnete = 0; // azzerato a ogni passaggio del magnete volatile uint32_t istanteMagnete = 0; // millis() dell'ultimo passaggio volatile bool magneteVisto = false; float frequenzaAttuale = 0.0f; // passi/s in questo momento float frequenzaObiettivo = 0.0f; // passi/s a regime bool timerAttivo = false; float ledAttuale = 0.0f; // % bool forzatoAcceso = false; // pressione lunga: acceso anche fuori orario bool forzatoSpento = false; // pressione breve: fermo anche in orario int giriDallaPausa = 0; int tentativi = 0; bool pausaRichiesta = false; uint32_t istanteStato = 0; uint32_t istanteAvvioGiro = 0; bool orologioValido = false; // ============================================================================ // GENERAZIONE DEI PASSI // ============================================================================ void IRAM_ATTR suTimerPassi(void*) { livelloStep = !livelloStep; digitalWrite(PIN_STEP, livelloStep); if (livelloStep) passiDalMagnete++; } void impostaFrequenza(float hz) { if (hz < 5.0f) { // sotto 5 passi/s consideriamo il motore fermo if (timerAttivo) { esp_timer_stop(timerPassi); timerAttivo = false; } frequenzaAttuale = 0.0f; return; } uint64_t semiperiodo = (uint64_t)(500000.0f / hz); // microsecondi if (timerAttivo) esp_timer_stop(timerPassi); esp_timer_start_periodic(timerPassi, semiperiodo); timerAttivo = true; frequenzaAttuale = hz; } void IRAM_ATTR suMagnete() { uint32_t ora = millis(); if (ora - istanteMagnete < 2000) return; // antirimbalzo: un solo impulso per passaggio istanteMagnete = ora; passiDalMagnete = 0; magneteVisto = true; } float periodoImpostato() { if (!USA_POTENZIOMETRO) return PERIODO_DEFAULT_S; int lettura = analogRead(PIN_POT); // 0..4095 return PERIODO_MAX_S - (PERIODO_MAX_S - PERIODO_MIN_S) * (lettura / 4095.0f); } // ============================================================================ // OROLOGIO DS3231 (tenuto in UTC) // ============================================================================ uint8_t bcdInDec(uint8_t v) { return (v >> 4) * 10 + (v & 0x0F); } uint8_t decInBcd(uint8_t v) { return ((v / 10) << 4) | (v % 10); } // secondi dal 1970 a partire da una data UTC (senza usare funzioni dipendenti dal fuso) time_t utcInEpoch(int anno, int mese, int giorno, int ora, int minuto, int secondo) { static const int cumulati[12] = { 0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334 }; long giorni = (anno - 1970) * 365L + (anno - 1969) / 4 - (anno - 1901) / 100 + (anno - 1601) / 400; giorni += cumulati[mese - 1] + (giorno - 1); bool bisestile = (anno % 4 == 0 && anno % 100 != 0) || (anno % 400 == 0); if (bisestile && mese > 2) giorni += 1; return (time_t)giorni * 86400L + ora * 3600L + minuto * 60L + secondo; } bool leggiRTC() { Wire.beginTransmission(0x68); Wire.write(0x00); if (Wire.endTransmission() != 0) return false; if (Wire.requestFrom(0x68, 7) != 7) return false; int s = bcdInDec(Wire.read() & 0x7F); int mi = bcdInDec(Wire.read()); int h = bcdInDec(Wire.read() & 0x3F); Wire.read(); // giorno della settimana, non usato int g = bcdInDec(Wire.read()); int me = bcdInDec(Wire.read() & 0x1F); int a = 2000 + bcdInDec(Wire.read()); if (a < 2025) return false; // orologio mai regolato o pila scarica struct timeval tv = { utcInEpoch(a, me, g, h, mi, s), 0 }; settimeofday(&tv, NULL); return true; } void scriviRTC(time_t epoch) { struct tm t; gmtime_r(&epoch, &t); Wire.beginTransmission(0x68); Wire.write(0x00); Wire.write(decInBcd(t.tm_sec)); Wire.write(decInBcd(t.tm_min)); Wire.write(decInBcd(t.tm_hour)); Wire.write(decInBcd(t.tm_wday + 1)); Wire.write(decInBcd(t.tm_mday)); Wire.write(decInBcd(t.tm_mon + 1)); Wire.write(decInBcd(t.tm_year - 100)); Wire.endTransmission(); } void sincronizzaNTP() { if (strlen(WIFI_SSID) == 0) return; WiFi.mode(WIFI_STA); WiFi.begin(WIFI_SSID, WIFI_PASSWORD); uint32_t inizio = millis(); while (WiFi.status() != WL_CONNECTED && millis() - inizio < 15000) delay(250); if (WiFi.status() == WL_CONNECTED) { configTzTime(FUSO_ORARIO, SERVER_NTP); struct tm t; if (getLocalTime(&t, 10000)) { scriviRTC(time(NULL)); orologioValido = true; Serial.println("Ora sincronizzata da NTP e scritta sul DS3231"); } } WiFi.disconnect(true); WiFi.mode(WIFI_OFF); // radio spenta durante il funzionamento } bool inOrario() { if (!orologioValido) return true; // senza orologio: sempre acceso, comanda il tasto struct tm t; time_t adesso = time(NULL); localtime_r(&adesso, &t); if (!GIORNI_ATTIVI[t.tm_wday]) return false; int minuti = t.tm_hour * 60 + t.tm_min; int inizio = ORA_ACCENSIONE * 60 + MIN_ACCENSIONE; int fine = ORA_SPEGNIMENTO * 60 + MIN_SPEGNIMENTO; return minuti >= inizio && minuti < fine; } // ============================================================================ // LUCI E TASTO // ============================================================================ void aggiornaLed(float obiettivo, float dt) { float passo = 100.0f / DISSOLVENZA_S * dt; if (ledAttuale < obiettivo) ledAttuale = min(obiettivo, ledAttuale + passo); else if (ledAttuale > obiettivo) ledAttuale = max(obiettivo, ledAttuale - passo); // curva quadratica: la dissolvenza appare lineare all'occhio float x = ledAttuale / 100.0f; analogWrite(PIN_LED, (int)(x * x * 255.0f + 0.5f)); } void leggiTasto() { static bool premuto = false; static uint32_t istantePressione = 0; bool ora = digitalRead(PIN_TASTO) == LOW; if (ora && !premuto) { premuto = true; istantePressione = millis(); } if (!ora && premuto) { premuto = false; uint32_t durata = millis() - istantePressione; if (durata > 1500) { // pressione lunga: forza acceso / torna all'orario forzatoAcceso = !forzatoAcceso; forzatoSpento = false; } else if (durata > 40) { // pressione breve: ferma / riparte if (stato == GUASTO) { tentativi = 0; stato = FERMO; } else forzatoSpento = !forzatoSpento; } } } // ============================================================================ // MACCHINA A STATI // ============================================================================ void entra(Stato nuovo) { stato = nuovo; istanteStato = millis(); } void abilitaMotore(bool si) { digitalWrite(PIN_ENABLE, si ? LOW : HIGH); } void aggiornaMoto(float dt) { bool deveGirare = !forzatoSpento && (forzatoAcceso || inOrario()); frequenzaObiettivo = PASSI_GIRO_PIATTAFORMA / periodoImpostato(); float accelerazione = frequenzaObiettivo / TEMPO_RAMPA_S; // passi/s^2 float periodoPrevistoMs = 1000.0f * PASSI_GIRO_PIATTAFORMA / max(frequenzaObiettivo, 1.0f); switch (stato) { case FERMO: if (deveGirare) { abilitaMotore(true); magneteVisto = false; istanteAvvioGiro = millis(); entra(AVVIO); } break; case AVVIO: impostaFrequenza(min(frequenzaObiettivo, max(frequenzaAttuale, 5.0f) + accelerazione * dt)); if (frequenzaAttuale >= frequenzaObiettivo) entra(MARCIA); if (!deveGirare) entra(ARRESTO); break; case MARCIA: if (fabsf(frequenzaAttuale - frequenzaObiettivo) > 1.0f) impostaFrequenza(frequenzaObiettivo); if (magneteVisto) { // un giro completato magneteVisto = false; istanteAvvioGiro = millis(); tentativi = 0; giriDallaPausa++; if (MODO_VETRINA && giriDallaPausa >= GIRI_TRA_LE_PAUSE) pausaRichiesta = true; } // blocco: il magnete non passa entro il tempo previsto if (millis() - istanteAvvioGiro > periodoPrevistoMs * TOLLERANZA_GIRO + TEMPO_RAMPA_S * 1000.0f) { impostaFrequenza(0); abilitaMotore(false); tentativi++; Serial.printf("Blocco rilevato (tentativo %d)\n", tentativi); entra(tentativi >= TENTATIVI_RIAVVIO ? GUASTO : ATTESA_RIAVVIO); break; } if (pausaRichiesta) { // inizia a frenare in modo da fermarsi all'angolo del prodotto di punta float passiFrenata = 0.5f * frequenzaAttuale * TEMPO_RAMPA_S; float passiPunta = ANGOLO_PUNTA_GRADI / 360.0f * PASSI_GIRO_PIATTAFORMA; if (passiDalMagnete >= passiPunta - passiFrenata && passiDalMagnete < passiPunta) { pausaRichiesta = false; giriDallaPausa = 0; entra(PAUSA_VETRINA); // frena, sosta, poi riparte break; } } if (!deveGirare) entra(ARRESTO); break; case PAUSA_VETRINA: if (frequenzaAttuale > 0.0f) { // fase di frenata impostaFrequenza(frequenzaAttuale - accelerazione * dt); if (frequenzaAttuale <= 0.0f) istanteStato = millis(); } else if (!deveGirare) { abilitaMotore(false); entra(FERMO); } else if (millis() - istanteStato > DURATA_PAUSA_S * 1000.0f) { magneteVisto = false; // il giro riparte da qui: concedi al controllo di blocco un giro intero in piu' istanteAvvioGiro = millis(); entra(AVVIO); } break; case ARRESTO: impostaFrequenza(frequenzaAttuale - accelerazione * dt); if (frequenzaAttuale <= 0.0f) { abilitaMotore(false); // motore senza corrente: silenzioso e freddo entra(FERMO); } break; case ATTESA_RIAVVIO: if (millis() - istanteStato > ATTESA_RIAVVIO_S * 1000.0f) entra(FERMO); break; case GUASTO: // resta fermo: serve una pressione breve del tasto break; } } // ============================================================================ // SETUP E LOOP // ============================================================================ void setup() { Serial.begin(115200); pinMode(PIN_STEP, OUTPUT); pinMode(PIN_DIR, OUTPUT); pinMode(PIN_ENABLE, OUTPUT); pinMode(PIN_LED, OUTPUT); pinMode(PIN_SPIA, OUTPUT); pinMode(PIN_HALL, INPUT); // GPIO34 non ha pull-up interno pinMode(PIN_TASTO, INPUT_PULLUP); abilitaMotore(false); digitalWrite(PIN_DIR, SENSO_ORARIO ? HIGH : LOW); analogWrite(PIN_LED, 0); esp_timer_create_args_t argomenti = {}; argomenti.callback = &suTimerPassi; argomenti.name = "passi"; esp_timer_create(&argomenti, &timerPassi); attachInterrupt(digitalPinToInterrupt(PIN_HALL), suMagnete, FALLING); Wire.begin(PIN_SDA, PIN_SCL); setenv("TZ", FUSO_ORARIO, 1); tzset(); orologioValido = leggiRTC(); sincronizzaNTP(); if (!orologioValido) Serial.println("Orologio non valido: funzionamento continuo, comandato dal tasto"); Serial.printf("Giostrina pronta. Passi per giro piattaforma: %.0f\n", PASSI_GIRO_PIATTAFORMA); } void loop() { static uint32_t ultimo = millis(); uint32_t adesso = millis(); if (adesso - ultimo < 20) return; // ciclo di controllo a 50 Hz float dt = (adesso - ultimo) / 1000.0f; ultimo = adesso; leggiTasto(); aggiornaMoto(dt); bool luciAccese = !forzatoSpento && (forzatoAcceso || inOrario()) && stato != GUASTO; aggiornaLed(luciAccese ? LED_GIORNO : LED_NOTTE, dt); // spia: fissa in marcia, lampeggio lento in pausa/fermo, veloce in guasto if (stato == GUASTO) digitalWrite(PIN_SPIA, (adesso / 150) % 2); else if (stato == MARCIA || stato == AVVIO) digitalWrite(PIN_SPIA, HIGH); else digitalWrite(PIN_SPIA, (adesso / 1000) % 2); static uint32_t ultimoRapporto = 0; if (adesso - ultimoRapporto > 5000) { ultimoRapporto = adesso; Serial.printf("stato=%d f=%.0f Hz periodo=%.0f s led=%.0f%% passi=%ld\n", (int)stato, frequenzaAttuale, periodoImpostato(), ledAttuale, (long)passiDalMagnete); } }