💡 Arduino • Scale • Automazione

Luci scale in sequenza con Arduino: accensione automatica bidirezionale

Realizziamo un sistema completo che accende progressivamente le luci dei gradini dal basso verso l’alto oppure dall’alto verso il basso, usando due pulsanti o due sensori di presenza.

DifficoltàIntermedia
Tempo45–60 minuti
Cosa impariSequenze, millis e stati
Come funziona: quando Arduino rileva una persona alla base della scala, accende i gradini dal primo all’ultimo. Se il passaggio viene rilevato in alto, la sequenza avviene al contrario. Dopo circa 10 secondi le luci si spengono progressivamente.

Configurazione scelta

La versione base controlla 10 gradini indipendenti. Per ogni gradino Arduino fornisce soltanto il segnale di comando: LED ad alta potenza, strisce LED o vere lampade devono essere pilotati attraverso MOSFET, driver o relè appropriati.

Gli ingressi possono essere due semplici pulsanti oppure due sensori PIR. Il software permette di scegliere il tipo di segnale modificando una sola costante.

Materiale necessario per il prototipo

🔌 Arduino Uno
💡 10 LED
Ω 10 resistenze da 220 Ω
🔘 2 pulsanti oppure 2 sensori PIR
🔗 Cavetti di collegamento
🧩 Breadboard

🧰 Kit Arduino consigliato

Un kit completo è utile per avere breadboard, cavetti, resistenze, LED e molti componenti di base per realizzare prima il prototipo a bassa tensione.

👉 Guarda il kit Arduino consigliato

Link di affiliazione Amazon: in qualità di Affiliato Amazon ricevo un guadagno dagli acquisti idonei, senza alcun costo aggiuntivo per te.

📘 Vuoi imparare Arduino passo dopo passo?

Se vuoi approfondire e realizzare progetti più completi, puoi dare un’occhiata anche ai miei libri dedicati ad Arduino. All’interno troverai spiegazioni semplici, schemi chiari e molti progetti pratici pensati per chi parte da zero.

👉 Scopri i miei libri su Arduino

Link di affiliazione Amazon: in qualità di Affiliato Amazon ricevo un guadagno dagli acquisti idonei, senza alcun costo aggiuntivo per te.

Collegamenti del prototipo

FunzionePin Arduino
Gradino 1D2
Gradino 2D3
Gradino 3D4
Gradino 4D5
Gradino 5D6
Gradino 6D7
Gradino 7D8
Gradino 8D9
Gradino 9D10
Gradino 10D11
Sensore/pulsante lato bassoA0
Sensore/pulsante lato altoA1
A0 e A1 come ingressi digitali: su Arduino Uno i pin analogici possono essere usati anche come normali ingressi digitali. In questo modo possiamo lasciare D2–D11 liberi per i dieci gradini.

Uso con due pulsanti

Collega ogni pulsante tra il relativo ingresso e GND. Il programma usa le resistenze di pull-up interne di Arduino, quindi non sono necessarie resistenze esterne sugli ingressi.

Modalità pulsanti
const bool INPUT_ACTIVE_LOW = true;

Uso con due sensori PIR

Se usi sensori di presenza con uscita digitale HIGH quando rilevano movimento, porta l’uscita del sensore su A0 o A1 e modifica:

Modalità sensori PIR
const bool INPUT_ACTIVE_LOW = false;

Codice Arduino completo

Luci scale bidirezionali con millis()
/*
  LUCI SCALE IN SEQUENZA CON ARDUINO UNO
  Versione completa bidirezionale e non bloccante

  FUNZIONAMENTO
  - Sensore/pulsante BASSO: accensione dal gradino 1 al gradino 10.
  - Sensore/pulsante ALTO: accensione dal gradino 10 al gradino 1.
  - Dopo un tempo di permanenza, le luci si spengono nella stessa direzione.
  - La sequenza usa millis(): Arduino continua a leggere gli ingressi.

  USCITE GRADINI:
  D2  -> gradino 1
  D3  -> gradino 2
  D4  -> gradino 3
  D5  -> gradino 4
  D6  -> gradino 5
  D7  -> gradino 6
  D8  -> gradino 7
  D9  -> gradino 8
  D10 -> gradino 9
  D11 -> gradino 10

  INGRESSI:
  A0 -> comando/sensore lato BASSO
  A1 -> comando/sensore lato ALTO

  Per pulsanti:
  - collega il pulsante tra A0/A1 e GND
  - lascia INPUT_ACTIVE_LOW = true

  Per sensori PIR con uscita HIGH:
  - collega OUT a A0/A1
  - imposta INPUT_ACTIVE_LOW = false

  IMPORTANTE:
  Le uscite Arduino NON devono alimentare direttamente lampade,
  strisce LED potenti o carichi a 230 V.
  Usa MOSFET, driver o relè adeguati.
*/

const byte NUM_GRADINI = 10;

// Pin delle 10 uscite
const byte luci[NUM_GRADINI] = {
  2, 3, 4, 5, 6,
  7, 8, 9, 10, 11
};

// Ingressi di comando
const byte INGRESSO_BASSO = A0;
const byte INGRESSO_ALTO  = A1;

// true  = pulsanti verso GND con INPUT_PULLUP
// false = sensori che portano HIGH quando rilevano presenza
const bool INPUT_ACTIVE_LOW = true;

// Se i tuoi moduli di uscita sono active LOW,
// inverti questi due valori.
const byte LUCE_ON  = HIGH;
const byte LUCE_OFF = LOW;

// Tempo tra un gradino e il successivo
const unsigned long TEMPO_PASSO = 180;

// Quanto restano accese tutte le luci
const unsigned long TEMPO_ACCESE = 10000;

// Antirimbalzo / filtro ingresso
const unsigned long DEBOUNCE_MS = 60;

enum Stato {
  ATTESA,
  ACCENSIONE,
  PERMANENZA,
  SPEGNIMENTO
};

Stato stato = ATTESA;

// +1 = dal basso verso l'alto
// -1 = dall'alto verso il basso
int direzione = +1;

// Indice usato durante le sequenze
int indice = 0;

unsigned long ultimoPasso = 0;
unsigned long inizioPermanenza = 0;

// Variabili per evitare retrigger continui
bool ultimoBasso = false;
bool ultimoAlto  = false;
unsigned long ultimoCambioBasso = 0;
unsigned long ultimoCambioAlto  = 0;

void setup() {

  // Configura tutte le uscite
  for (byte i = 0; i < NUM_GRADINI; i++) {
    pinMode(luci[i], OUTPUT);
    digitalWrite(luci[i], LUCE_OFF);
  }

  // Configurazione ingressi
  if (INPUT_ACTIVE_LOW) {
    pinMode(INGRESSO_BASSO, INPUT_PULLUP);
    pinMode(INGRESSO_ALTO, INPUT_PULLUP);
  } else {
    pinMode(INGRESSO_BASSO, INPUT);
    pinMode(INGRESSO_ALTO, INPUT);
  }
}

void loop() {

  // Legge continuamente i due ingressi
  bool triggerBasso = ingressoAttivo(
    INGRESSO_BASSO,
    ultimoBasso,
    ultimoCambioBasso
  );

  bool triggerAlto = ingressoAttivo(
    INGRESSO_ALTO,
    ultimoAlto,
    ultimoCambioAlto
  );

  switch (stato) {

    // ------------------------------------------------
    // ATTESA
    // ------------------------------------------------
    case ATTESA:

      if (triggerBasso) {
        avviaSequenza(+1);
      }
      else if (triggerAlto) {
        avviaSequenza(-1);
      }

      break;

    // ------------------------------------------------
    // ACCENSIONE PROGRESSIVA
    // ------------------------------------------------
    case ACCENSIONE:

      if (millis() - ultimoPasso >= TEMPO_PASSO) {

        ultimoPasso = millis();

        digitalWrite(luci[indice], LUCE_ON);

        indice += direzione;

        // Fine della sequenza di accensione
        if (
          indice >= NUM_GRADINI ||
          indice < 0
        ) {
          stato = PERMANENZA;
          inizioPermanenza = millis();
        }
      }

      break;

    // ------------------------------------------------
    // TUTTE LE LUCI RESTANO ACCESE
    // ------------------------------------------------
    case PERMANENZA:

      // Se passa qualcuno dall'altro lato mentre
      // le luci sono accese, prolunga il tempo.
      if (triggerBasso || triggerAlto) {
        inizioPermanenza = millis();
      }

      if (
        millis() - inizioPermanenza >= TEMPO_ACCESE
      ) {

        // Per uno spegnimento visivamente naturale
        // partiamo dallo stesso lato da cui
        // era iniziata l'accensione.
        indice = (direzione == +1)
               ? 0
               : NUM_GRADINI - 1;

        ultimoPasso = millis();

        stato = SPEGNIMENTO;
      }

      break;

    // ------------------------------------------------
    // SPEGNIMENTO PROGRESSIVO
    // ------------------------------------------------
    case SPEGNIMENTO:

      // Se qualcuno arriva mentre stiamo spegnendo,
      // riaccendiamo la scala nella sua direzione.
      if (triggerBasso) {
        avviaSequenza(+1);
        break;
      }

      if (triggerAlto) {
        avviaSequenza(-1);
        break;
      }

      if (millis() - ultimoPasso >= TEMPO_PASSO) {

        ultimoPasso = millis();

        digitalWrite(luci[indice], LUCE_OFF);

        indice += direzione;

        if (
          indice >= NUM_GRADINI ||
          indice < 0
        ) {
          stato = ATTESA;
        }
      }

      break;
  }
}

// ----------------------------------------------------
// Avvia la sequenza nella direzione richiesta
// ----------------------------------------------------
void avviaSequenza(int nuovaDirezione) {

  direzione = nuovaDirezione;

  indice = (direzione == +1)
         ? 0
         : NUM_GRADINI - 1;

  ultimoPasso = millis();

  stato = ACCENSIONE;
}

// ----------------------------------------------------
// Restituisce true SOLO sul fronte di attivazione
// dell'ingresso, con un piccolo filtro antirimbalzo.
// ----------------------------------------------------
bool ingressoAttivo(
  byte pin,
  bool &ultimoStato,
  unsigned long &ultimoCambio
) {

  bool lettura;

  if (INPUT_ACTIVE_LOW) {
    lettura = (digitalRead(pin) == LOW);
  } else {
    lettura = (digitalRead(pin) == HIGH);
  }

  // Se lo stato cambia, memorizziamo il momento
  if (lettura != ultimoStato) {

    if (millis() - ultimoCambio >= DEBOUNCE_MS) {

      ultimoCambio = millis();
      ultimoStato = lettura;

      // Genera trigger solo quando passa a "attivo"
      if (lettura) {
        return true;
      }
    }
  }

  return false;
}

Perché non usiamo una lunga serie di delay()

Con molti delay() Arduino resterebbe bloccato durante tutta la sequenza. In quel periodo non potrebbe reagire bene a una persona che entra dalla parte opposta della scala.

Il programma usa invece millis() e una piccola macchina a stati con quattro fasi:

  • ATTESA: nessuna sequenza in corso;
  • ACCENSIONE: le luci vengono attivate una alla volta;
  • PERMANENZA: tutte le luci restano accese;
  • SPEGNIMENTO: le luci vengono spente progressivamente.

Accensione dal basso

Quando viene attivato l’ingresso A0, Arduino parte dal gradino 1 e procede fino al gradino 10. Ogni uscita viene attivata dopo circa 180 ms dalla precedente.

Accensione dall’alto

Quando viene attivato A1, l’indice parte dall’ultimo gradino e procede a ritroso. La stessa scala può quindi reagire correttamente in entrambe le direzioni.

Tempo di permanenza

Quando tutti i gradini sono accesi, il programma attende 10 secondi prima di iniziare lo spegnimento.

Tempo luci accese
const unsigned long TEMPO_ACCESE = 10000;

Puoi aumentarlo, per esempio a 20000 per 20 secondi.

Velocità della sequenza

Ritardo tra i gradini
const unsigned long TEMPO_PASSO = 180;

Con valori inferiori la scala si illumina più velocemente. Con valori maggiori l’effetto diventa più lento e scenografico.

Cosa succede se arriva un’altra persona?

Durante la fase in cui tutte le luci sono accese, un nuovo rilevamento prolunga il tempo prima dello spegnimento. Se invece Arduino sta già spegnendo le luci e rileva un nuovo passaggio, interrompe lo spegnimento e avvia nuovamente la sequenza nella direzione richiesta.

Questo è il vantaggio principale della versione non bloccante: il sistema continua a controllare gli ingressi anche mentre sta gestendo l’animazione.

Passare dai LED alle vere luci dei gradini

Per un impianto reale consiglierei strisce LED a bassa tensione, ad esempio una sezione indipendente per ogni gradino. Ogni uscita di Arduino dovrebbe comandare un MOSFET logic-level dimensionato per la corrente del relativo tratto LED.

Non alimentare le luci dai pin Arduino: ogni pin deve fornire soltanto il segnale di controllo. Alimentatore, MOSFET, cablaggio e protezioni devono essere dimensionati in base alla potenza totale delle strisce.

E se la scala ha più di 10 gradini?

La soluzione più ordinata è aggiungere espansori di uscita, per esempio registri a scorrimento o moduli I/O, invece di occupare un pin Arduino per ogni gradino. Il software può mantenere la stessa logica bidirezionale.

Versione con dissolvenza

Per ottenere un effetto ancora più elegante si può sostituire la semplice accensione ON/OFF con una dissolvenza progressiva. Per molti gradini, però, conviene usare un driver PWM multicanale perché Arduino Uno dispone di un numero limitato di uscite PWM hardware.

Debug rapido

  • prova inizialmente il circuito con dieci LED e resistenze da 220 Ω;
  • verifica separatamente il comando dal basso e quello dall’alto;
  • se usi pulsanti, lascia INPUT_ACTIVE_LOW = true;
  • se usi PIR con uscita HIGH, imposta INPUT_ACTIVE_LOW = false;
  • se un modulo relè lavora al contrario, inverti LUCE_ON e LUCE_OFF;
  • modifica TEMPO_PASSO per regolare la velocità;
  • modifica TEMPO_ACCESE per regolare il tempo di permanenza.

Varianti del progetto

  • aggiungere un sensore crepuscolare per funzionare solo al buio;
  • usare due sensori ToF invece dei PIR;
  • aggiungere dissolvenza graduale;
  • controllare 15 o 20 gradini con un’espansione di uscite;
  • aggiungere una modalità luce notturna permanente;
  • impostare velocità diversa tra accensione e spegnimento;
  • aggiungere ESP32 per controllo e configurazione via Wi-Fi.

Cosa devi ricordare

  • La versione base controlla 10 gradini.
  • A0 rileva il passaggio dal basso, A1 quello dall’alto.
  • La direzione di accensione dipende dal lato da cui arriva la persona.
  • La sequenza è gestita con millis() e non blocca Arduino.
  • I nuovi rilevamenti possono prolungare o riavviare la sequenza.
  • Per strisce LED reali servono driver o MOSFET adeguati.

Domande frequenti

Posso usare due pulsanti invece dei sensori?

Sì. Il codice è già predisposto. Collega ciascun pulsante tra A0/A1 e GND e lascia INPUT_ACTIVE_LOW = true.

Posso usare due sensori PIR?

Sì. Collega le uscite digitali dei PIR agli ingressi A0 e A1 e, se forniscono HIGH quando rilevano movimento, imposta INPUT_ACTIVE_LOW = false.

Posso controllare più di 10 gradini?

Sì. Per numeri superiori conviene usare un’espansione di uscite, così non si esauriscono rapidamente i pin di Arduino Uno.

Posso collegare direttamente una striscia LED ai pin Arduino?

No. Usa un MOSFET o un driver adeguato e un alimentatore separato dimensionato per la corrente richiesta.