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:
- LED1 su D9: lampeggia ogni 500 ms;
- seriale: stampa un messaggio ogni 1000 ms;
- 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 conmillis(). 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 🙂
