Archivi tag: millis()

Esercitazione 4 – Macchina a stati per un ciclo automatico con start, pausa, allarme e reset

Allenamento per l’esame di maturità
Percorso di laboratorio con Arduino per studenti di quinta ITIS

Obiettivo didattico

Modellare un ciclo automatico come macchina a stati: attesa, marcia, pausa, allarme e reset. L’attività avvicina gli studenti al modo in cui si progetta un processo tecnico da esame.

Materiali suggeriti

  • Arduino UNO R3 o UNO R4,
  • 3 pulsanti (START, PAUSA, RESET);
  • 2 LED;
  • 2 Resistori da 220 Ohm (per i LED);
  • jumper.

Schema di collegamento

Richiamo teorico

Una macchina a stati finiti rappresenta un processo come un insieme di stati mutuamente esclusivi. In ogni stato il sistema esegue azioni ben definite e attende eventi che causano il passaggio allo stato successivo. Questo metodo riduce gli errori logici nei programmi complessi.

Schema logico dell’attività

  • All’avvio il sistema è in ATTESA;
  • se arriva START passa a MARCIA;
  • in MARCIA può ricevere PAUSA oppure entrare in ALLARME se scade il tempo massimo;
  • in PAUSA attende una nuova pressione di START;
  • da ALLARME si esce solo con RESET, che riporta il sistema in ATTESA.

Diagramma a stati

Per rappresentare in modo corretto il comportamento di questo esercizio è molto utile affiancare al codice un diagramma a stati. In questo caso, infatti, il sistema non si limita a eseguire istruzioni una dopo l’altra, ma cambia comportamento in funzione della condizione operativa in cui si trova.

Il programma può trovarsi in un solo stato per volta, e ogni stato descrive una precisa fase di funzionamento del sistema. Nel nostro sketch gli stati sono quattro:

  • STATO_ATTESA
  • STATO_MARCIA
  • STATO_PAUSA
  • STATO_ALLARME

Il diagramma a stati è particolarmente adatto a questa situazione perché mostra in modo immediato tre aspetti fondamentali:

  • quali sono gli stati possibili del sistema;
  • quali eventi provocano il passaggio da uno stato all’altro;
  • quale logica di funzionamento è stata implementata nel codice.

In altre parole, mentre il diagramma di flusso descrive bene la sequenza dei controlli svolti nel loop(), il diagramma a stati descrive meglio la struttura logica del processo.

Stato iniziale del sistema

Nel diagramma compare il simbolo iniziale che punta verso STATO_ATTESA. Questo significa che, all’accensione di Arduino o dopo un reset, il sistema parte nello stato di attesa.

In questa fase il ciclo automatico non è attivo. Il LED di marcia è spento, il LED di allarme è spento e il programma controlla continuamente i pulsanti in attesa di un comando di avvio.

Dal punto di vista didattico, questo è il comportamento tipico di molti sistemi automatici: il processo non parte in modo autonomo, ma richiede un comando esplicito dell’operatore.

Transizione da ATTESA a MARCIA

Dal nodo STATO_ATTESA parte una freccia verso STATO_MARCIA con l’etichetta:

START premuto / istanteInizioMarcia = millis()

Questa notazione va letta così: se il sistema si trova in attesa e viene rilevata la pressione del pulsante START, allora il programma passa allo stato di marcia. Durante questa transizione viene anche eseguita un’azione importante, cioè il salvataggio dell’istante iniziale della marcia tramite millis().

Questo passaggio è fondamentale perché permette al sistema di iniziare a contare il tempo trascorso in marcia senza bloccare il programma. In pratica, nel momento in cui parte la marcia, viene memorizzato il riferimento temporale da cui inizierà il controllo del timeout.

Significato dello stato MARCIA

Lo stato STATO_MARCIA rappresenta la fase in cui il sistema è attivo.

In questa condizione il LED di marcia viene acceso, il LED di allarme resta spento e il programma continua a controllare due possibili situazioni:

    • la richiesta di pausa;
    • il superamento del tempo massimo consentito.

Importante: nello stato di marcia il sistema non esegue una sola azione, ma resta in una condizione operativa in cui il programma continua a verificare gli eventi che possono modificare il comportamento complessivo del processo.

Transizione da MARCIA a PAUSA

Se durante la marcia viene premuto il pulsante PAUSA, il sistema passa a STATO_PAUSA.

Questa transizione rappresenta una sospensione del ciclo. Dal punto di vista logico, il processo non è terminato, ma è momentaneamente fermo in attesa di un nuovo comando.

Nel diagramma questa freccia mostra che la pausa può essere richiesta solo quando il sistema si trova effettivamente in marcia. Questo rende molto chiaro un concetto fondamentale: non tutti i comandi hanno senso in tutti gli stati. Il significato di un ingresso dipende dallo stato corrente del sistema.

Transizione da MARCIA a ALLARME

La seconda uscita possibile da STATO_MARCIA conduce a STATO_ALLARME ed è attivata dal superamento del tempo massimo di marcia.

Nel codice questa condizione è realizzata confrontando il tempo attuale, ottenuto con millis(), con l’istante iniziale salvato all’ingresso nello stato di marcia. Quando la differenza raggiunge o supera il valore impostato in TEMPO_MASSIMO_MARCIA, il sistema genera l’allarme.

Questo passaggio è interessante perché mostra come una macchina a stati possa cambiare stato non solo in risposta a un pulsante, ma anche in risposta a una condizione temporale. In molti sistemi automatici reali, infatti, il tempo costituisce un vincolo di sicurezza o di controllo del processo.

Significato dello stato PAUSA

Lo stato STATO_PAUSA rappresenta una sospensione controllata del ciclo.

In questa fase il LED di marcia è spento e il LED di allarme è spento. Il programma non torna allo stato iniziale, ma resta in una condizione intermedia da cui il processo può riprendere.

Dal diagramma si vede che da questo stato esiste una sola transizione utile: la pressione di START, che riporta il sistema in STATO_MARCIA.

È importante però osservare un dettaglio del codice: quando si torna in marcia dalla pausa, il programma esegue di nuovo l’istruzione che salva istanteInizioMarcia = millis(). Questo significa che il conteggio del tempo massimo non viene congelato e poi ripreso, ma riparte da zero.

Questo aspetto è importante, perché aiuta a capire che il diagramma descrive la struttura generale del comportamento, mentre le variabili e le azioni associate alle transizioni determinano il comportamento concreto del sistema.

Significato dello stato ALLARME

Lo stato STATO_ALLARME rappresenta una condizione anomala o di sicurezza.

Quando il sistema entra in allarme, il LED di marcia viene spento e il LED di allarme viene acceso. In questo modo l’operatore può riconoscere immediatamente che il ciclo si è interrotto per il superamento del limite previsto.

Dal diagramma si vede che da STATO_ALLARME non si può tornare direttamente in marcia. L’unica uscita prevista è verso STATO_ATTESA, e questa transizione è attivata dal pulsante RESET.

Anche questa è una scelta molto importante dal punto di vista progettuale: in presenza di un allarme il sistema non riparte automaticamente, ma richiede un intervento esplicito da parte dell’operatore. È una logica tipica dei sistemi in cui la sicurezza o il controllo delle anomalie hanno priorità sulla continuità del funzionamento.

IMPORTANTE

Quando si studiano le macchine a stati è utile distinguere tra:

      • azioni di stato, cioè ciò che il sistema fa mentre si trova in un determinato stato;
      • azioni di transizione, cioè ciò che il sistema esegue nel momento in cui passa da uno stato a un altro.

Nell’esercizio proposto, l’accensione o lo spegnimento dei LED, è un’azione associata allo stato, perché avviene nel blocco di codice relativo a ciascun case.

Il salvataggio di istanteInizioMarcia, invece, è un’azione di transizione, perché viene eseguito nel momento in cui il sistema entra nello stato di marcia.
Questa distinzione è molto utile per comprendere meglio sia il diagramma sia il codice.

Continua a leggere

Esercitazione 3 – Scheduler cooperativo con tre task e supervisione dei tempi

Allenamento per l’esame di maturità
Percorso di laboratorio con Arduino per studenti di quinta ITIS

Obiettivo didattico

Organizzare il programma come scheduler cooperativo con tre task indipendenti: acquisizione analogica, lampeggio di stato e trasmissione seriale periodica. L’attività mostra come il loop() possa diventare un piccolo supervisore software.

Materiali suggeriti

  • Arduino UNO R3 o UNO R4;
  • 1 potenziometro;
  • 2 LED;
  • 2 resitori (per i LED);
  • breadboard;
  • cavetti jumper.

Schema di collegamento

Richiamo teorico

In un sistema embedded semplice non si usa un vero sistema operativo, ma si può costruire uno scheduler cooperativo con millis(). Ogni task possiede il proprio intervallo e il proprio istante dell’ultima esecuzione. Il loop() controlla se ciascun task è pronto e lo richiama.

Schema logico dell’attività

Il programma inizializza i timer dei tre task. Nel loop legge il tempo corrente e verifica uno dopo l’altro se è il momento di eseguire il task di acquisizione, quello di segnalazione e quello di stampa seriale. Se il tempo non è scaduto, passa al controllo successivo.

Diagramma di flusso

Diagramma di flusso Mermaid

flowchart TD
    A[Inizio] --> B[Configura pin, seriale e timer]
    B --> C[Leggi tempo attuale]
    C --> D{Task acquisizione pronto?}
    D -- Sì --> E[Esegui acquisizione]
    D -- No --> F{Task LED pronto?}
    E --> F
    F -- Sì --> G[Commuta LED di stato]
    F -- No --> H{Task seriale pronto?}
    G --> H
    H -- Sì --> I[Invia dati in seriale]
    H -- No --> J[Ritorna al loop]
    I --> J
    J --> C

Programma

/*
  Prof. Maffucci Michele
  Esercizio 3: scheduler cooperativo con tre task indipendenti
*/

const int PIN_SENSORE = A0;
const int PIN_LED_STATO = 8;
const int PIN_LED_SOGLIA = 9;

// ---------------------------
// Intervalli dei tre task
// ---------------------------
const unsigned long PERIODO_ACQUISIZIONE = 50;
const unsigned long PERIODO_LED = 300;
const unsigned long PERIODO_SERIALE = 500;

// ---------------------------
// Istanti ultima esecuzione
// ---------------------------
unsigned long ultimoTaskAcquisizione = 0;
unsigned long ultimoTaskLed = 0;
unsigned long ultimoTaskSeriale = 0;

// ---------------------------
// Variabili condivise
// ---------------------------
int valoreGrezzo = 0;
float tensione = 0.0;
bool statoLed = false;

void setup() {
  pinMode(PIN_LED_STATO, OUTPUT);
  pinMode(PIN_LED_SOGLIA, OUTPUT);
  Serial.begin(9600);
}

void loop() {
  // Il loop assume il ruolo di piccolo scheduler.
  unsigned long adesso = millis();

  // ---------------------------
  // Task 1: acquisizione
  // ---------------------------
  if ((adesso - ultimoTaskAcquisizione) >= PERIODO_ACQUISIZIONE) {
    ultimoTaskAcquisizione = adesso;
    taskAcquisizione();
  }

  // ---------------------------
  // Task 2: LED di stato
  // ---------------------------
  if ((adesso - ultimoTaskLed) >= PERIODO_LED) {
    ultimoTaskLed = adesso;
    taskLed();
  }

  // ---------------------------
  // Task 3: seriale
  // ---------------------------
  if ((adesso - ultimoTaskSeriale) >= PERIODO_SERIALE) {
    ultimoTaskSeriale = adesso;
    taskSeriale();
  }
}

// ----------------------------------------------------------
// Legge il sensore e calcola una tensione equivalente.
// ----------------------------------------------------------
void taskAcquisizione() {
  valoreGrezzo = analogRead(PIN_SENSORE);

  // Conversione esplicita ADC -> tensione.
  tensione = (valoreGrezzo * 5.0) / 1023.0;

  // Uso della misura per attivare un LED di soglia.
  if (tensione >= 2.50) {
    digitalWrite(PIN_LED_SOGLIA, HIGH);
  } else {
    digitalWrite(PIN_LED_SOGLIA, LOW);
  }
}

// ----------------------------------------------------------
// Task di segnalazione periodica.
// ----------------------------------------------------------
void taskLed() {
  if (statoLed == false) {
    statoLed = true;
    digitalWrite(PIN_LED_STATO, HIGH);
  } else {
    statoLed = false;
    digitalWrite(PIN_LED_STATO, LOW);
  }
}

// ----------------------------------------------------------
// Task di comunicazione seriale.
// ----------------------------------------------------------
void taskSeriale() {
  Serial.print("ADC = ");
  Serial.print(valoreGrezzo);
  Serial.print("  Tensione = ");
  Serial.print(tensione, 2);
  Serial.println(" V");
}

Continua a leggere

Esercitazione 1 – Pulsante singolo con antirimbalzo, doppio clic, pressione lunga e timeout

Allenamento per l’esame di maturità
Percorso di laboratorio con Arduino per studenti di quinta ITIS

Obiettivo didattico

Realizzare una piccola interfaccia utente con un solo pulsante. Il sistema deve riconoscere una pressione breve, un doppio clic, una pressione lunga e un timeout di inattività. L’attività allena la gestione degli eventi, l’antirimbalzo software e l’uso di millis() senza bloccare il programma.

Nota importante

La comprensione del funzionamento di questo esercizio permetterà lo svolgimento in autonomia dell’esercizio aggiuntivo che trovate al fondo di questa scheda di lavoro.

Materiali suggeriti

  • Arduino UNO R3 o UNO R4;
  • 1 pulsante;
  • 3 LED;
  • 3 resitori (per i LED);
  • breadboard;
  • cavetti jumper.

Schema di collegamento

Richiamo teorico

Per leggere correttamente un pulsante reale bisogna evitare il rimbalzo dei contatti. Si usa quindi una variabile con l’ultima lettura, una temporizzazione di stabilizzazione e una logica a eventi. Con millis() si misura il tempo senza usare delay(), così il programma può continuare a controllare altri compiti.

Schema logico dell’attività

Il programma legge continuamente il pulsante. Se la lettura cambia, attende il tempo di debounce. Quando il livello è stabile, riconosce pressione e rilascio. Dal tempo trascorso ricava l’evento: clic breve, doppio clic oppure pressione lunga. Se non accade nulla per molti secondi, attiva un LED di timeout.

Diagramma di flusso

Diagramma di flusso Mermaid

Ricordo che per realizzare il diagramma di flusso, copiate il codice Mermaid seguendo le indicazioni della lezione: Progettare bene, programmare meglio: diagrammi di flusso – cos’è il formato Mermaid? – Lezione 2/5

flowchart TD
    A[Inizio] --> B[Configura pin e variabili]
    B --> C[Leggi pulsante]
    C --> D{Lettura cambiata?}
    D -- Sì --> E[Salva istante variazione]
    D -- No --> F{Tempo debounce trascorso?}
    E --> F
    F -- No --> C
    F -- Sì --> G{Stato stabile cambiato?}
    G -- No --> H{Timeout inattività?}
    G -- Sì --> I{Pulsante premuto?}
    I -- Sì --> J[Memorizza istante pressione]
    I -- No --> K[Calcola durata pressione]
    K --> L{Pressione lunga?}
    L -- Sì --> M[Genera evento LONG]
    L -- No --> N{Secondo clic entro finestra?}
    N -- Sì --> O[Genera evento DOUBLE]
    N -- No --> P[Attendi possibile secondo clic]
    H -- Sì --> Q[Attiva LED timeout]
    H -- No --> C
    J --> C
    M --> C
    O --> C
    P --> C
    Q --> C

Programma

/*
  Prof. Maffucci Michele
  Esercizio 1: Pulsante singolo con antirimbalzo, doppio clic, pressione lunga e timeout
*/


// ---------------------------
// Definizione dei pin usati
// ---------------------------
const int PIN_PULSANTE = 2;
const int PIN_LED_BREVE = 8;
const int PIN_LED_DOPPIO = 9;
const int PIN_LED_TIMEOUT = 10;

// ---------------------------
// Costanti temporali
// ---------------------------
const unsigned long TEMPO_DEBOUNCE = 30;
const unsigned long SOGLIA_PRESSIONE_LUNGA = 800;
const unsigned long FINESTRA_DOPPIO_CLICK = 350;
const unsigned long TEMPO_TIMEOUT = 5000;

// ---------------------------
// Variabili per antirimbalzo
// ---------------------------
int ultimaLetturaGrezza = HIGH;
int statoStabile = HIGH;
unsigned long istanteUltimaVariazione = 0;

// ---------------------------
// Variabili per eventi utente
// ---------------------------
bool attesaSecondoClick = false;
unsigned long istantePrimoClick = 0;
unsigned long istantePressione = 0;
unsigned long ultimoEventoUtente = 0;

void setup() {
  // Il pulsante usa la resistenza interna di pull-up.
  pinMode(PIN_PULSANTE, INPUT_PULLUP);

  // I tre LED rappresentano tre eventi diversi.
  pinMode(PIN_LED_BREVE, OUTPUT);
  pinMode(PIN_LED_DOPPIO, OUTPUT);
  pinMode(PIN_LED_TIMEOUT, OUTPUT);

  // All'avvio tutti i LED sono spenti.
  digitalWrite(PIN_LED_BREVE, LOW);
  digitalWrite(PIN_LED_DOPPIO, LOW);
  digitalWrite(PIN_LED_TIMEOUT, LOW);

  // La seriale aiuta a vedere quale evento viene riconosciuto.
  Serial.begin(9600);

  // Salvo il tempo iniziale come ultimo evento.
  ultimoEventoUtente = millis();
}

void loop() {
  // Leggo il pulsante in forma grezza.
  int letturaCorrente = digitalRead(PIN_PULSANTE);

  // Se la lettura è cambiata rispetto alla precedente,
  // aggiorno il tempo della variazione.
  if (letturaCorrente != ultimaLetturaGrezza) {
    istanteUltimaVariazione = millis();
    ultimaLetturaGrezza = letturaCorrente;
  }

  // Se il segnale è stabile da abbastanza tempo,
  // posso considerarlo affidabile.
  if ((millis() - istanteUltimaVariazione) >= TEMPO_DEBOUNCE) {

    // Se anche lo stato stabile è cambiato, ho un nuovo evento.
    if (letturaCorrente != statoStabile) {
      statoStabile = letturaCorrente;
      ultimoEventoUtente = millis();
      digitalWrite(PIN_LED_TIMEOUT, LOW);

      // Transizione verso livello basso = pulsante premuto.
      if (statoStabile == LOW) {
        istantePressione = millis();
      }
      // Transizione verso livello alto = pulsante rilasciato.
      else {
        unsigned long durataPressione = millis() - istantePressione;

        // Se la durata supera la soglia, classifico come pressione lunga.
        if (durataPressione >= SOGLIA_PRESSIONE_LUNGA) {
          Serial.println("Evento: PRESSIONE LUNGA");
          lampeggiaLed(PIN_LED_DOPPIO, 2, 120);
          attesaSecondoClick = false;
        }
        else {
          // Se sto già aspettando un secondo clic,
          // verifico la finestra temporale.
          if (attesaSecondoClick == true &&
              (millis() - istantePrimoClick) <= FINESTRA_DOPPIO_CLICK) {
            Serial.println("Evento: DOPPIO CLICK");
            lampeggiaLed(PIN_LED_DOPPIO, 1, 250);
            attesaSecondoClick = false;
          }
          else {
            // Primo clic breve: non lo confermo subito,
            // perché potrei ricevere un secondo clic.
            attesaSecondoClick = true;
            istantePrimoClick = millis();
          }
        }
      }
    }
  }

  // Se è in attesa un secondo clic e la finestra è scaduta,
  // confermo il clic breve.
  if (attesaSecondoClick == true && (millis() - istantePrimoClick) > FINESTRA_DOPPIO_CLICK) {
    Serial.println("Evento: CLICK BREVE");
    lampeggiaLed(PIN_LED_BREVE, 1, 250);
    attesaSecondoClick = false;
  }

  // Se passa troppo tempo senza eventi, attivo il LED di timeout.
  if ((millis() - ultimoEventoUtente) >= TEMPO_TIMEOUT) {
    digitalWrite(PIN_LED_TIMEOUT, HIGH);
  }
}

// ----------------------------------------------------------
// Funzione di servizio: fa lampeggiare un LED alcune volte.
// In questa attività è accettabile usare delay() perché
// la funzione serve solo come feedback visivo di conferma.
// ----------------------------------------------------------
void lampeggiaLed(int pinLed, int numeroLampi, int durata) {
  for (int i = 0; i < numeroLampi; i = i + 1) {
    digitalWrite(pinLed, HIGH);
    delay(durata);
    digitalWrite(pinLed, LOW);
    delay(durata);
  }
}

Continua a leggere

Multitasking con Arduino – lezione 2/3

Nella lezione precedente abbiamo visto come organizzare uno sketch in task indipendenti (lampeggio LED, stampa su Serial, lettura pulsante) usando millis() al posto di delay().

In questa seconda lezione aggiungiamo un attuatore: un servomotore SG90, mantenendo la stessa logica non bloccante. L’obiettivo è far capire che anche un movimento meccanico può essere gestito “in parallelo” agli altri processi, senza congelare l’esecuzione.

Specifiche

LED e pulsante

  • LED1 (D9): lampeggia ogni 500 ms (task temporizzato).
  • Serial: stampa un messaggio ogni 1000 ms (task temporizzato).
  • Pulsante (D2): configurato con INPUT_PULLUP
    • a riposo legge HIGH
    • premuto (verso GND) legge LOW
  • Alla pressione del pulsante:
    • LED2 (D8) si accende
    • parte la sequenza del servo 0° > 90° > 0°

Servo SG90

  • Movimento gestito a micro-step (es. 2° ogni 15 ms), usando millis():
    • movimento fluido,
    • il movimento non blocca gli altri task.

Collegamenti del servo SG90

  • Rosso > 5V
  • Marrone/Nero > GND
  • Arancione (segnale) > D10

Nota pratica: un SG90 può generare picchi di corrente. Se noti reset o instabilità, usa un’alimentazione 5V separata per il servo con GND in comune con Arduino.
Nota tecnica (UNO classico): la libreria Servo usa un timer; evita di contare su analogWrite() (PWM) su alcuni pin (in particolare 9/10 su UNO) quando usi Servo.

Esempio 01: spegnimento LED attivazione servo automatico a fine sequenza

/*
    Prof. Maffucci Michele
    23.02.26

    Multitasking senza delay()
      - TASK 1: LED1 su D9 lampeggia ogni 500 ms (uso di millis)
      - TASK 2: Messaggio su Serial ogni 1000 ms (uso di millis)
      - TASK 3: Pulsante su D2 (INPUT_PULLUP) con debounce:
                - alla pressione ACCENDE LED2 (D8)
                - avvia la sequenza del servo 0° -> 90° -> 0° (se non già in corso)
      - TASK 4: Movimento servo non bloccante a piccoli step temporizzati.
                Quando la sequenza termina, LED2 si SPEGNE automaticamente.
*/

#include <Servo.h> 


// -----------------------
// PIN HARDWARE
// -----------------------
#define PIN_LED_LAMPEGGIO  9     // LED1 (lampeggio)
#define PIN_LED_PULSANTE   8     // LED2 (spia attività: ON durante sequenza servo)
#define PIN_PULSANTE       2     // Pulsante collegato a GND (INPUT_PULLUP)
#define PIN_SERVO         10     // Segnale servo SG90

// -----------------------
// TIMING (millis) - Task temporizzati
// -----------------------
unsigned long tempoPrecedenteLed    = 0;  // riferimento per TASK 1
unsigned long tempoPrecedenteSerial = 0;  // riferimento per TASK 2

const unsigned long INTERVALLO_LED    = 500;
const unsigned long INTERVALLO_SERIAL = 1000;

// -----------------------
// STATI LED
// -----------------------
bool statoLedLampeggio = LOW;

// -----------------------
// SERVO e parametri sequenza
// -----------------------
Servo servoSg90;

bool movimentoServoAttivo = false;  // true mentre il servo sta eseguendo la sequenza
int angoloServo = 0;                // angolo corrente
int direzioneServo = +1;            // +1 sale verso 90°, -1 scende verso 0°

unsigned long tempoPrecedenteServo = 0;       // riferimento per TASK 4
const unsigned long INTERVALLO_SERVO = 15;    // ms tra uno step e il successivo
const int PASSO_SERVO = 2;                    // gradi per step
const int ANGOLO_MAX = 90;                    // angolo massimo della sequenza

void setup() {
  pinMode(PIN_LED_LAMPEGGIO, OUTPUT);
  pinMode(PIN_LED_PULSANTE,  OUTPUT);

  // INPUT_PULLUP: il pin è tenuto HIGH internamente.
  // Quando premi il pulsante (verso GND) la lettura diventa LOW.
  pinMode(PIN_PULSANTE, INPUT_PULLUP);

  // Stati iniziali
  digitalWrite(PIN_LED_LAMPEGGIO, statoLedLampeggio);
  digitalWrite(PIN_LED_PULSANTE,  LOW);       // LED2 parte spento

  // Servo: inizializzo e porto a 0°
  servoSg90.attach(PIN_SERVO);
  servoSg90.write(0);

  Serial.begin(9600);
}

void loop() {
  // Il loop è lo "scheduler cooperativo": richiama spesso i task.
  // Ogni task decide autonomamente se è il momento di agire.
  taskLampeggioLed();    // TASK 1
  taskStampaSeriale();   // TASK 2
  taskLeggiPulsante();   // TASK 3
  taskMovimentoServo();  // TASK 4
}

// =====================================================
// TASK 1: Lampeggio LED1 (non bloccante)
// =====================================================
void taskLampeggioLed() {
  // Se è passato l'intervallo, invertiamo lo stato del LED
  if (millis() - tempoPrecedenteLed >= INTERVALLO_LED) {
    tempoPrecedenteLed = millis();
    statoLedLampeggio = !statoLedLampeggio;
    digitalWrite(PIN_LED_LAMPEGGIO, statoLedLampeggio);
  }
}

// =====================================================
// TASK 2: Stampa su Serial (non bloccante)
// =====================================================
void taskStampaSeriale() {
  // Stampa periodica indipendente dagli altri task
  if (millis() - tempoPrecedenteSerial >= INTERVALLO_SERIAL) {
    tempoPrecedenteSerial = millis();
    Serial.println("Multitasking: LED blink + pulsante + servo (senza delay)!");
  }
}

// =====================================================
// TASK 3: Lettura pulsante + debounce + avvio eventi
//
// - "Debounce": il contatto del pulsante rimbalza per alcuni millisecondi.
//   Qui accettiamo il cambio di stato solo se resta stabile per 50 ms.
// - Evento: gestiamo l'azione solo sul fronte di pressione (quando diventa LOW).
// - Azioni su pressione:
//   1) Accende LED2 (spia attività)
//   2) Avvia la sequenza servo (se non già in corso)
// =====================================================
void taskLeggiPulsante() {
  // static: mantengono il valore tra una chiamata e l'altra (task sempre richiamato nel loop)
  static bool ultimaLettura = HIGH;          // lettura grezza precedente (può rimbalzare)
  static bool statoStabile  = HIGH;          // stato validato dopo debounce
  static unsigned long ultimoCambio = 0;     // quando è cambiata l'ultimaLettura

  const unsigned long RITARDO_DEBOUNCE = 50;

  bool letturaAttuale = digitalRead(PIN_PULSANTE);

  // Se la lettura grezza cambia, ripartiamo col conteggio del tempo di stabilità
  if (letturaAttuale != ultimaLettura) {
    ultimoCambio = millis();
    ultimaLettura = letturaAttuale;
  }

  // Se la lettura resta invariata abbastanza, la consideriamo "stabile"
  if (millis() - ultimoCambio >= RITARDO_DEBOUNCE) {
    // Cambio reale dello stato stabile
    if (letturaAttuale != statoStabile) {
      statoStabile = letturaAttuale;

      // INPUT_PULLUP: premuto = LOW
      // Eseguiamo l'azione solo quando il pulsante viene premuto
      if (statoStabile == LOW) {

        // 1) LED2: spia attività -> ON quando parte la sequenza
        digitalWrite(PIN_LED_PULSANTE, HIGH);
        Serial.println("Pulsante premuto: LED2 (D8) ACCESO (sequenza servo in corso)");

        // 2) Avvia la sequenza del servo solo se non è già attiva
        if (!movimentoServoAttivo) {
          movimentoServoAttivo = true;
          angoloServo = 0;
          direzioneServo = +1;

          // Porto subito a 0° e aggancio il timing del task servo
          servoSg90.write(angoloServo);
          tempoPrecedenteServo = millis();

          Serial.println("Avvio sequenza servo: 0° -> 90° -> 0°");
        }
      }
    }
  }
}

// =====================================================
// TASK 4: Movimento servo (non bloccante)
//
// La sequenza è gestita a piccoli step temporizzati:
// - ogni INTERVALLO_SERVO ms, angoloServo cambia di PASSO_SERVO gradi
// - quando raggiunge 90°, inverte direzione
// - quando torna a 0°, la sequenza termina
//
// Importante: qui NON c'è delay(), quindi gli altri task continuano a funzionare.
// A fine sequenza, LED2 si spegne automaticamente.
// =====================================================
void taskMovimentoServo() {
  if (!movimentoServoAttivo) return;

  if (millis() - tempoPrecedenteServo >= INTERVALLO_SERVO) {
    tempoPrecedenteServo = millis();

    // Aggiorna angolo in base alla direzione (salita/discesa)
    angoloServo += direzioneServo * PASSO_SERVO;

    // Raggiunto il massimo: blocca a 90° e inverte direzione
    if (angoloServo >= ANGOLO_MAX) {
      angoloServo = ANGOLO_MAX;
      direzioneServo = -1;
    }

    // Tornato a 0° in discesa: fine sequenza
    if (angoloServo <= 0 && direzioneServo == -1) {
      angoloServo = 0;
      servoSg90.write(0);

      movimentoServoAttivo = false;
      Serial.println("Sequenza servo completata (ritorno a 0°)");

      // Correzione richiesta: LED2 si spegne automaticamente a fine sequenza
      digitalWrite(PIN_LED_PULSANTE, LOW);
      Serial.println("LED2 (D8) SPENTO: fine sequenza servo");

      return;
    }

    // Applica l'angolo al servo
    servoSg90.write(angoloServo);
  }
}


Continua a leggere

Multitasking con Arduino – lezione 1/3

Per chi lavora con Arduino da un po’ di tempo, prima o poi si imbatte in questa frase letta online: delay() è il “male assoluto” :-). In realtà non è delay() in sé il problema: il punto è l’abuso che spesso se ne fa. Ogni volta che lo inseriamo nel codice, Arduino si ferma per tutto il tempo indicato, smettendo di leggere pulsanti, aggiornare uscite, gestire sensori o comunicazioni. In progetti semplici può anche andare bene, ma appena aumentano le funzioni da gestire “in parallelo”, quel blocco diventa un collo di bottiglia che rende il comportamento poco reattivo e difficile da far crescere.

Quando iniziamo a realizzare progetti con più componenti elettronici che interagiscono (LED che lampeggiano, pulsanti, messaggi su seriale, sensori…), delay() diventa subito un problema: blocca tutto. Se mettete un delay(500), per mezzo secondo Arduino non fa nient’altro: non legge pulsanti, non aggiorna altre uscite, non gestisce eventi.

La soluzione classica è passare a una logica non bloccante usando millis() e dividendo il programma in task (piccole funzioni) che vengono richiamate continuamente nel loop().

Ho trattato più volte dell’uso del millis() su questo sito, vi rimando al post: “Guida all’uso di millis() – Lezione 1” in cui ne mostro l’utilizzo in diverse situazioni.

Nel primo esempio realizziamo 3 processi (task) simultanei:

  1. LED1 su D9: lampeggia ogni 500 ms;
  2. seriale: stampa un messaggio ogni 1000 ms;
  3. pulsante su D2: ad ogni pressione commuta (ON/OFF) LED2 su D8 con debounce.

Per i collegamenti seguire lo schema che segue.

  • LED1: anodo > D9, catodo > resistenza 220 Ω > GND
  • LED2: anodo > D8, catodo > resistenza 220 Ω > GND
  • Pulsante: un capo > D2, l’altro capo > GND

Nota importante: non inseriamo la resistenza di pull-up esterna da 10 kΩ, perché abilitiamo la resistenza di pull-up interna con INPUT_PULLUP, pertanto a riposo il pin legge HIGH, premuto legge LOW.

L’idea di base è che ogni task controlla il suo tempo, quindi useremo una variabile che memorizza l’ultimo istante in cui ha fatto qualcosa.
Quando millis() - ultimoTempo >= intervallo, allora esegue l’azione e aggiorna ultimoTempo.
Ciò permette di far funzionare il ciclo di loop di Arduino senza nessun blocco e nessun task blocca gli altri task.

/*
    Prof. Maffucci Michele
    16.02.26

    Multitasking senza delay()
    - LED su D9 lampeggia ogni 500 ms
    - Stampa su Serial ogni 1000 ms
    - Pulsante su D2 (INPUT_PULLUP) commuta un LED su D8
*/

// Pin hardware
#define PIN_LED_LAMPEGGIO 9
#define PIN_LED_PULSANTE  8
#define PIN_PULSANTE      2

// "Timestamp" (in millisecondi) dell'ultima esecuzione dei task temporizzati
unsigned long tempoPrecedenteLed    = 0;
unsigned long tempoPrecedenteSerial = 0;

// Intervalli di esecuzione dei task (in millisecondi)
const unsigned long INTERVALLO_LED    = 500;
const unsigned long INTERVALLO_SERIAL = 1000;

// Stato logico dei LED
bool statoLedLampeggio = LOW;   // LED su D9
bool statoLedPulsante  = LOW;   // LED su D8

void setup() {
  // Impostiamo i pin dei LED come uscite
  pinMode(PIN_LED_LAMPEGGIO, OUTPUT);
  pinMode(PIN_LED_PULSANTE, OUTPUT);

  // Pulsante con pull-up interna:
  // - a riposo legge HIGH
  // - premuto (verso GND) legge LOW
  pinMode(PIN_PULSANTE, INPUT_PULLUP);

  // Stato iniziale delle uscite (opzionale ma consigliato per chiarezza)
  digitalWrite(PIN_LED_LAMPEGGIO, statoLedLampeggio);
  digitalWrite(PIN_LED_PULSANTE,  statoLedPulsante);

  Serial.begin(9600);
}

void loop() {
  // Il loop richiama continuamente i task.
  // Ogni task decide autonomamente "se è il momento" di fare qualcosa.
  taskLampeggioLed();
  taskStampaSeriale();
  taskLeggiPulsante();
}

// -------------------------------
// TASK 1: Lampeggio LED (non bloccante)
// -------------------------------
void taskLampeggioLed() {
  // Se è passato almeno INTERVALLO_LED dall'ultima volta, invertiamo lo stato del LED
  if (millis() - tempoPrecedenteLed >= INTERVALLO_LED) {
    tempoPrecedenteLed = millis();              // aggiorno l'istante di riferimento
    statoLedLampeggio = !statoLedLampeggio;     // toggle
    digitalWrite(PIN_LED_LAMPEGGIO, statoLedLampeggio);
  }
}

// -------------------------------
// TASK 2: Stampa su Serial (non bloccante)
// -------------------------------
void taskStampaSeriale() {
  // Stampa un messaggio ogni INTERVALLO_SERIAL millisecondi
  if (millis() - tempoPrecedenteSerial >= INTERVALLO_SERIAL) {
    tempoPrecedenteSerial = millis();
    Serial.println("Multitasking: 3 task attivi (senza delay)!");
  }
}

// -------------------------------
// TASK 3: Lettura pulsante + debounce + commutazione LED2
// -------------------------------
void taskLeggiPulsante() {
  // Variabili statiche: mantengono il valore tra una chiamata e la successiva.
  // Servono perché questa funzione viene richiamata di continuo nel loop().
  static bool ultimaLettura = HIGH;                 // ultima lettura "grezza" (non filtrata)
  static bool statoPulsanteStabile = HIGH;          // stato stabilizzato dopo debounce
  static unsigned long ultimoCambio = 0;            // quando è cambiata l'ultimaLettura

  const unsigned long RITARDO_DEBOUNCE = 50;        // tempo minimo per considerare stabile un cambio

  bool letturaAttuale = digitalRead(PIN_PULSANTE);  // lettura istantanea (può "rimbalzare")

  // Se la lettura è cambiata rispetto a prima, resettiamo il timer di debounce
  if (letturaAttuale != ultimaLettura) {
    ultimoCambio = millis();
    ultimaLettura = letturaAttuale;
  }

  // Se la lettura resta invariata per almeno RITARDO_DEBOUNCE ms,
  // possiamo considerarla stabile e aggiornare lo stato "ufficiale" del pulsante.
  if (millis() - ultimoCambio >= RITARDO_DEBOUNCE) {
    // Se lo stato stabile del pulsante è cambiato davvero...
    if (letturaAttuale != statoPulsanteStabile) {
      statoPulsanteStabile = letturaAttuale;

      // Con INPUT_PULLUP: premuto = LOW
      // Eseguiamo la commutazione SOLO sul fronte di pressione (quando diventa LOW).
      if (statoPulsanteStabile == LOW) {
        statoLedPulsante = !statoLedPulsante;                 // toggle LED2
        digitalWrite(PIN_LED_PULSANTE, statoLedPulsante);
      }
    }
  }
}
  • taskLampeggioLed()
    Controlla il tempo con millis(). Quando sono passati 500 ms, inverte lo stato del LED su D9.
  • taskStampaSeriale()
    Ogni 1000 ms stampa un messaggio. Anche qui: nessun delay(), quindi non blocca la lettura del pulsante.
  • taskLeggiPulsante()
    Legge D2 con pull-up interna. Implementa un debounce software:

    • registra quando la lettura cambia,
    • aspetta 50 ms di stabilità,
    • considera valido il cambio e, solo quando il pulsante viene premuto (LOW), commuta LED2.

Realizziamo ora una seconda versione dello sketch in cui:

  • Task 4: “fade” PWM non bloccante su un pin PWM (D10).
  • Stampa su Serial dello stato di LED2 (D8) ogni volta che viene commutato dal pulsante.

Collegamento aggiuntivo: un terzo LED (con resistenza 220 Ω) su un pin PWM (quelli con il simbolo tilde ~ sulla scheda). Nell’esempio utilizzo D10.

/*
    Prof. Maffucci Michele
    16.02.26

    Multitasking senza delay()
    - Task 1: LED su D9 lampeggia ogni 500 ms
    - Task 2: Stampa su Serial ogni 1000 ms
    - Task 3: Pulsante su D2 (INPUT_PULLUP) commuta LED su D8 + stampa stato
    - Task 4: Fade PWM su un pin PWM (es. D10) senza bloccare nulla
*/


// Pin hardware
#define PIN_LED_LAMPEGGIO   9     // LED1
#define PIN_LED_PULSANTE    8     // LED2
#define PIN_PULSANTE        2     // Pulsante verso GND (INPUT_PULLUP)
#define PIN_LED_FADE_PWM   10     // LED3 (serve un pin PWM: ~)

// Timestamp (millis) per i task temporizzati
unsigned long tempoPrecedenteLed     = 0;
unsigned long tempoPrecedenteSerial  = 0;
unsigned long tempoPrecedenteFadePwm = 0;

// Intervalli (ms)
const unsigned long INTERVALLO_LED     = 500;
const unsigned long INTERVALLO_SERIAL  = 1000;
const unsigned long INTERVALLO_FADEPWM = 10;   // velocità aggiornamento fade (più basso = più fluido/rapido)

// Stati
bool statoLedLampeggio = LOW;   // LED su D9
bool statoLedPulsante  = LOW;   // LED su D8

// Parametri fade PWM (0..255 su molte schede Arduino)
int  luminositaPwm = 0;         // livello corrente
int  passoPwm      = 5;         // quanto cambia ad ogni aggiornamento (es. 1..10)
bool aumentaPwm    = true;      // direzione del fade

void setup() {
  pinMode(PIN_LED_LAMPEGGIO, OUTPUT);
  pinMode(PIN_LED_PULSANTE,  OUTPUT);

  // Pull-up interna: a riposo HIGH, premuto LOW (pulsante collegato a GND)
  pinMode(PIN_PULSANTE, INPUT_PULLUP);

  // Pin PWM: lo usiamo con analogWrite() (non serve pinMode obbligatorio, ma lo mettiamo per chiarezza)
  pinMode(PIN_LED_FADE_PWM, OUTPUT);

  // Stato iniziale delle uscite
  digitalWrite(PIN_LED_LAMPEGGIO, statoLedLampeggio);
  digitalWrite(PIN_LED_PULSANTE,  statoLedPulsante);
  analogWrite(PIN_LED_FADE_PWM,   luminositaPwm);

  Serial.begin(9600);
}

void loop() {
  // Ogni task viene richiamato continuamente.
  // Nessuno usa delay(): ogni task decide da solo quando agire.
  taskLampeggioLed();
  taskStampaSeriale();
  taskLeggiPulsante();
  taskFadePwm();
}

// -------------------------------
// TASK 1: Lampeggio LED (non bloccante)
// -------------------------------
void taskLampeggioLed() {
  if (millis() - tempoPrecedenteLed >= INTERVALLO_LED) {
    tempoPrecedenteLed = millis();
    statoLedLampeggio = !statoLedLampeggio;
    digitalWrite(PIN_LED_LAMPEGGIO, statoLedLampeggio);
  }
}

// -------------------------------
// TASK 2: Stampa su Serial (non bloccante)
// -------------------------------
void taskStampaSeriale() {
  if (millis() - tempoPrecedenteSerial >= INTERVALLO_SERIAL) {
    tempoPrecedenteSerial = millis();
    Serial.println("Multitasking cooperativo: 4 task attivi (senza delay)!");
  }
}

// -------------------------------
// TASK 3: Lettura pulsante + debounce + commutazione LED2
//       + stampa dello stato di LED2 quando cambia
// -------------------------------
void taskLeggiPulsante() {
  static bool ultimaLettura = HIGH;            // lettura grezza precedente
  static bool statoStabile  = HIGH;            // stato stabilizzato (debounce)
  static unsigned long ultimoCambio = 0;       // momento dell'ultimo cambio lettura

  const unsigned long RITARDO_DEBOUNCE = 50;

  bool letturaAttuale = digitalRead(PIN_PULSANTE);

  // Se la lettura grezza cambia, aggiorniamo il timer di debounce
  if (letturaAttuale != ultimaLettura) {
    ultimoCambio = millis();
    ultimaLettura = letturaAttuale;
  }

  // Se la lettura resta stabile per un tempo sufficiente, la accettiamo come valida
  if (millis() - ultimoCambio >= RITARDO_DEBOUNCE) {
    if (letturaAttuale != statoStabile) {
      statoStabile = letturaAttuale;

      // Con INPUT_PULLUP: premuto = LOW
      // Commutiamo LED2 solo sul fronte di pressione (quando diventa LOW)
      if (statoStabile == LOW) {
        statoLedPulsante = !statoLedPulsante;
        digitalWrite(PIN_LED_PULSANTE, statoLedPulsante);

        // Stampa su Serial dello stato di LED2 al momento della commutazione
        Serial.print("LED2 (D8) ora e': ");
        Serial.println(statoLedPulsante ? "ACCESO" : "SPENTO");
      }
    }
  }
}

// -------------------------------
// TASK 4: Fade PWM (non bloccante) su pin PWM
// -------------------------------

void taskFadePwm() {
  // Aggiorniamo la luminosità con una cadenza regolare (INTERVALLO_FADEPWM).
  // Così l'effetto è fluido e, soprattutto, non blocca gli altri task.
  if (millis() - tempoPrecedenteFadePwm >= INTERVALLO_FADEPWM) {
    tempoPrecedenteFadePwm = millis();

    // Aggiorna luminosità in base alla direzione
    if (aumentaPwm) {
      luminositaPwm += passoPwm;
      if (luminositaPwm >= 255) {             // limite alto (classico 8-bit)
        luminositaPwm = 255;
        aumentaPwm = false;                   // inverti direzione
      }
    } else {
      luminositaPwm -= passoPwm;
      if (luminositaPwm <= 0) {               // limite basso
        luminositaPwm = 0;
        aumentaPwm = true;                    // inverti direzione
      }
    }

    // Applica PWM (luminosità percepita del LED)
    analogWrite(PIN_LED_FADE_PWM, luminositaPwm);
  }
}

Il fade PWM è un esempio perfetto per capire il multitasking su Arduino, perché l’effetto sembra continuo e fluido, quasi come se Arduino stesse “dimmerando” il LED in tempo reale senza occuparsi d’altro. In realtà non c’è nessuna operazione continua: il programma aggiorna la luminosità a piccoli passi (ad esempio +5 o −5) a intervalli regolari (per esempio ogni 10 ms), usando millis() per decidere quando fare l’aggiornamento successivo. Tra un passo e l’altro il loop() continua a girare e può eseguire gli altri task (lampeggio, lettura pulsante, seriale). È proprio questa alternanza rapida di micro-azioni temporizzate che crea l’illusione del “continuo”, mantenendo però il codice reattivo e capace di gestire più funzioni “in parallelo” senza blocchi e senza delay().

Buon Making a tutti 🙂