Archivi tag: pwm

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 🙂

Esercitazioni di base di Arduino – visualizzazione luminosità LED mediante una barra di avanzamento su display 16×2


Continuo nell’aggiornamento delle esercitazioni di base per gli studenti di 3′ dell’ITIS (Elettronica ed Automazione) e per gli utenti che iniziano con Arduino.
Utilizzeremo un display LCD 16×2 di tipo I2C su cui visualizzeremo mediante una barra di avanzamento, la quantità di luminosità impostata per il LED, mediante un potenziometro.

Per la gestione di un display LCD di tipo I2C rimando alla mia lezione: Utilizzo dell’LCD 16×2 Hitachi HD44780 1602 con modulo I2C PCF8574T.

Per la creazione di caratteri personalizzati rimando alla mia lezione: Disegnare caratteri personalizzati con Arduino per un LCD 16×2.

Lista componenti

  • N.1 Arduino UNO
  • N.1 Breadboard
  • N.1 LCD 16×2 I2C
  • N.1 Potenziometri da 10 KOhm
  • N.1 LED da 5 mm
  • N.1 Resistore da 220 Ohm
  • jumper

Schema di collegamento

Scketch

Di seguito viene indicato il codice i base, all’interno i commenti che ne dettagliano il funzionamento di ogni parte:

/*
   Prof. Maffucci Michele
   https://www.maffucci.it
   Ver.1 - 27.12.21
   Controllo di luminosità LED con
   visualizzazione intensità mediante una
   barra di avanzamento su display 16x2
*/

#include <Wire.h>
#include <LiquidCrystal_I2C.h>

byte pinPot = A0;           // pin analogico 0 a cui connettere il potenziometro per controllare la luminosità
byte pinLed = 3;            // pin PWM a cui connettere il LED
int  analogVal = 0;         // variabile in cui memorizzare il valore impostato dal potenziometro
int  luminosita = 0;        // variabile in cui memorizzare la luminosità
byte barraAvanzamento = 0;  // indice barra avanzamento

// Per maggiori informazioni sulla realizzazione di caratteri speciali:
// https://www.maffucci.it/2020/01/18/disegnare-caratteri-personalizzati-con-arduino-per-un-lcd-16x2/

// Carattere personalizzato per disegnare la barraAvanzamento di avanzamento
byte iconaBarra[8] = {
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
  B11111,
};

// inizializzazione della libreria in cui è descritta la modalità di utilizzo dei pin
// impostazione dell'indirizzo dell'LCD 0x27 di 16 caratteri e 2 linee
LiquidCrystal_I2C lcd(0x27, 16, 2);
//-----------------------------

void setup()
{
  lcd.begin();      // inizializzazione dell'LCD
  lcd.backlight();  // attivazione della retroilluminazione

  // Inpostazione ad OUTPUT del pin a cui connettiamo il LED
  pinMode(pinLed, OUTPUT);

  // Cancella il display
  lcd.clear();

  // Stampa il messaggio sulla prima riga del display
  lcd.print("Luminosita' LED");

  //Creazione del carattere per la barra di avanzamento
  lcd.createChar(0, iconaBarra);
}

// Per maggiori informazioni sull'uso del display 16x2 I2C:
// https://www.maffucci.it/2019/01/25/utilizzo-delllcd-16x2-hitachi-hd44780-1602-con-modulo-i2c-pcf8574t/
//-----------------------------

void loop() {
  // Cancella il display
  lcd.clear();

  // Stampa il messsaggio sulla prima riga
  lcd.print("Luminosita' LED");

  //Posiziona il cursore nella seconda riga, prima colonna
  lcd.setCursor(0,1);

  // Lettura del valore impostato dal potenziometro
  analogVal = analogRead(pinPot);

  // Conversione del valore analogico impostato con il potenziometro
  // in Duty Cicle per impostare la luminosità del LED
  luminosita=map(analogVal, 0, 1024, 0, 255);

  // Impostazione della luminosità del LED
  analogWrite(pinLed, luminosita);

  // Conversione della luminosità in quantità di caratteri della barra da stampare
  barraAvanzamento=map(luminosita, 0, 255, 0, 15);

  // Stampa la barra di avanzamento
  for (byte i = 0; i < barraAvanzamento; i++)
  {
    lcd.setCursor(i, 1);
    lcd.write(byte(0));
  }
  // leggero ritardo di 500 ms per visualizzare la barra
  delay(500);
}

Proposta esercizi

Esercizio 1
Nell’esempio proposto viene utilizzato un delay() finale per permettere la visualizzazione dei caratteri sul display. Sostituire il delay() ed utilizzare l’istruzione millis() per valutare il tempo trascorso e controllare la stampa dei caratteri sul display.

Esercizio 2
Modificare il programma precedente raddoppiando il numero di caratteri che rappresenta la barra di avanzamento.

Esercizio 3
Modificare l’esercizio proposto variando la barra di avanzamento in funzione dell’approssimarsi al valore massimo o minimo della luminosità.

Esercizio 4
Modificare l’esercizio proposte inserendo un buzzer che emette un suono la cui frequenza varia in funzione dell’intensità luminosa del LED.

Buon Making a tutti 🙂

Arduino: controllare il movimento di un motore a spazzola con un transistor

Abbiamo visto a lezione in cosa consiste la Modulazione di Larghezza di Impulso, in altro modo conosciuta come PWM (Pulse Width Modulation). Questo tipo di modulazione permette ad un sistema digitale di controllare dispositivi di tipo analogico in corrente continua facendo variare in modo continuo la potenza erogata, quindi moduliamo la larghezza di un impulso, ovvero la durata temporale di una serie di impulsi che regolerà l’attivazione e disattivazione del motore. Con il PWM agiamo non sulla tensione di alimentazione per controllare la velocità del motore, ma bensì sul tempo, per maggiori informazioni vi rimando alle slide: Alfabeto Arduino – Lezione 2 in cui come esempio viene controllata l’intensità luminosa di LED. (Per i miei studenti fate riferimento al libro di testo e agli appunti di teoria).

Il transistor nell’immagine che segue viene fatto funzionare in modalità ON-OFF e quindi  possiamo assimilarlo ad un interruttore che si apre e si chiude in corrispondenza del segnale presente sulla base del transistor. Se il segnale sulla base è alto il transistor sarà in conduzione e il motore potrà essere alimentato (si chiude verso massa il collegamento del motore). Se il segnale sulla base del transistor è a livello basso il transistor è assimilabile ad un interruttore aperto per cui il motore risulta non alimentato (non si ha collegamento a massa). Quindi al motore verrà applicata una tensione continua proporzionale al duty cycle e quindi variando il duty cycle varieremo la velocità di rotazione del motore.

La generazione dell’onda quadra, che controlla la velocità del motore, viene eseguita da Arduino. Il diodo presente nel circuito è chiamato diodo di ricircolo ed è inserito in parallelo al motore (carico induttivo) per sopprimere i transienti elettrici ed è collegato in modo tale che non conduce quando il carico (il motore) viene alimentato.
Quando il motore viene disattivato rapidamente si avrà un picco di tensione in senso inverso perché l’induttore tenderà a mantenere costante la corrente che circola al suo interno, in questa situazione però il diodo sarà polarizzato direttamente e tutta la corrente scorrerà dall’induttore al diodo e l’energia che era stata immagazzinata dall’induttore  viene dissipata in calore dalla componente resistiva dell’induttore. Inserendo il diodo si eviterà di applicare una sovratensione sul collettore del transistor evitando quindi che si danneggi.

Controlliamo con Arduino il movimento del motore

Lista Componenti

  • Arduino UNO R3
  • Resistore da 1 K Ohm
  • Dido: 1N4001
  • Transistor: 2N2222
  • Condensatore: 0,1 microF

Pinout transistor 2N2222

Circuito 1 di montaggio: alimentazione del motore prelevata da Arduino

Circuito 2 di montaggio: alimentazione del motore esterna ad Arduino

Esempio 1
Si faccia riferimento al circuito 1. Realizziamo uno sketch che permette la regolazione della velocità del motore inserendo un numero compreso tra 0 e 9. Il valore 0 ferma il motore, il valore 9 permette di muovere il motore alla velocità massima, valori intermedi movimenteranno il motore ad una velocità proporzionale al numero inserito.

Valori fuori dall’intervallo impostato restituiscono un messaggio di errore.

Per la realizzazione di questo sketch si utilizza la funzione isDigit() che restituisce TRUE verifica se il carattere inviato sulla Serial Monitor è un numero, altrimenti restituisce FALSE.

/*
   Prof. Maffucci Michele
   Controllo motore a spazzola:
   accensione, spegnimento, controllo velocità.
   All'avvio del programma il motore è spento.

   La velocità del motore è impostata
   con un valore compreso tra 0 e 9

   Valori non numerici restituiscono
   un messaggio di errore

   Data: 19.01.2021
*/

// driver del motore collegato al pin 6
const byte pinMotore = 6; // motor driver is connected to pin 3

// per stampare una sola volta il messaggio sulla Serial Monitor
bool abilitaMessaggio = 0;

void setup()
{
  // inizializzazione della serial monitor
  Serial.begin(9600);

  // inizializzazione OUTPUT del pin 6
  pinMode(pinMotore, OUTPUT);
}

void loop()
{
  // consente di visualizzare sulla Serial Monitor
  // una sola stampa delle stringa
  if (abilitaMessaggio == 0) {
    // ritardo che evita la doppia stampa del messaggio
    delay(200);
    Serial.println("Controllo velocità motore");
    Serial.println("Inserire la velocità (0 - 9)");
    Serial.println();
    abilitaMessaggio = 1;
  }

  // Controlla se è disponibile almeno un carattere sulla seriale
  // La Serial.available() restituisce
  // 1 se presente un cattere,
  // 0 se non è presente un carattere

  // per maggior informazioni sull'uso di parseInt() consultare il link:
  // https://wp.me/p4kwmk-4Ah

  if (Serial.available()) {        // Viene controllato se è disponibile un carattere

    // definizione di una variabile di tipo char in cui memorizzare
    // il carattere inviato ad Arduino mediante la Serial Monitor

    char carattere = Serial.read();

    // La funzione isDigit restituisce TRUE se se il carattere
    // inviato sulla Serial Monitor è un numero altrimenti restituisce FALSE

    if (isDigit(carattere)) // verifica se è un numero
    {
      // mappiamo l'intervallo dei caratteri da '0' a '9'
      // in un valore compreso tra 0 e 255, intervallo di valori
      // del Duty Cycle

      int vel = map(carattere, '0', '9', 0, 255);
      analogWrite(pinMotore, vel);

      Serial.print("Valore del Duty Cycle: ");
      Serial.println(vel);
      Serial.println("--------------------------");
      Serial.print("Velocità impostata: ");
      Serial.println(carattere);
      Serial.println("==========================");

    }
    else
    {
      // nel caso in cui il carattere inserito non è un numero
      // viene restituito un messaggio e stampa il carattere
      Serial.println();
      Serial.println("******************************");
      Serial.print("Carattere non riconosciuto: ");
      Serial.println(carattere);
      Serial.println("******************************");
      Serial.println();
    }
  }
}

Esempio 2
Si faccia riferimento al circuito 1. Realizziamo uno sketch che permette la regolazione della velocità del motore inserendo un numero compreso tra 0 e 9. Il valore 0 ferma il motore, il valore 9 permette di muovere il motore alla velocità massima, valori intermedi movimenteranno il motore ad una velocità proporzionale al numero inserito.

Valori fuori dall’intervallo impostato fermano il motore.

Per la realizzazione di questo sketch si utilizza la funzione Serial.parseInt() che legge i caratteri sulla seriali e restituisce la loro rappresentazione numerica (tipo long). I caratteri che non sono numeri interi (o con segno meno) vengono ignorati.

Nel dettaglio

  • I caratteri iniziali che non sono cifre o sono numeri negativi vengono ignorati;
  • L’analisi si interrompe quando non sono stati letti caratteri per un valore di tempo di timeout che può essere configurato oppure viene letta una non cifra;
  • Se non sono state lette cifre valide quando si verifica il timeout (vedere Serial.setTimeout ()), viene restituito 0; Serial.parseInt () eredita dalla classe Stream.
/*
   Prof. Maffucci Michele
   Controllo motore a spazzola:
   accensione, spegnimento, controllo velocità.
   All'avvio del programma il motore è spento.

   La velocità del motore è impostata
   con un valore compreso tra 0 e 9

   Valori non numerici fermano il motore.

   Viene utilizzata la funzione Serial.parseInt() per leggere

   Data: 19.01.2021

*/

// driver del motore collegato al pin 6
const byte pinMotore = 6; // motor driver is connected to pin 3

// per stampare una sola volta il messaggio sulla Serial Monitor
bool abilitaMessaggio = 0;

void setup()
{
  // inizializzazione della serial monitor
  Serial.begin(9600);

  // inizializzazione OUTPUT del pin 6
  pinMode(pinMotore, OUTPUT);
}

void loop()
{
  // consente di visualizzare sulla Serial Monitor
  // una sola stampa delle stringa
  if (abilitaMessaggio == 0) {
    // ritardo che evita la doppia stampa del messaggio
    delay(200);
    Serial.println("Controllo velocità motore");
    Serial.println("Inserire la velocità (0 - 9)");
    Serial.println();
    abilitaMessaggio = 1;
  }

  // Controlla se è disponibile almeno un carattere sulla seriale
  // La Serial.available() restituisce
  // 1 se presente un cattere,
  // 0 se non è presente un carattere

  // per maggior informazioni sull'uso di parseInt() consultare il link:
  // https://wp.me/p4kwmk-4Ah

  if (Serial.available()) {        // Viene controllato se è disponibile un carattere

    // definizione di una variabile di tipo char in cui memorizzare
    // il carattere inviato ad Arduino mediante la Serial Monitor

    // per maggior informazioni sull'uso di parseInt() consultare il link:
    // https://wp.me/p4kwmk-4Ah

    int valore = Serial.parseInt();

    // La funzione isDigit verifica se il carattere inviato è un numero
    // e restituisce TRUE se il carattere è un numero altrimenti restituisce FALSE

    // mappiamo l'intervallo dei caratteri da '0' a '9'
    // in un valore compreso tra 0 e 255, intervallo di valori
    // del Duty Cycle

    if (valore >= 0 && valore <= 9) {

      if (valore == 0) {
        Serial.println();
        Serial.println("************************************");
        Serial.println("MOTORE FERMO");
        Serial.println("Valore inserito 0 o fuori intervallo");
        Serial.println("************************************");
        Serial.println();
      }

      int vel = map(valore, 0, 9, 0, 255);
      analogWrite(pinMotore, vel);

      Serial.print("Valore del Duty Cycle: ");
      Serial.println(vel);
      Serial.println("--------------------------");
      Serial.print("Velocità impostata: ");
      Serial.println(valore);
      Serial.println("==========================");
    }
  }
}

Esercizio 1
Aggiungere al circuito 1 due pulsanti che permettono di aumentare o diminuire la velocità del motore. Fare in modo che la velocità impostata del motore sia proporzionale ai valori numerici interi nell’intervallo tra 0 e 9, così come fatto negli esempi precedenti. Ad ogni pressione del pulsante P1 si incrementa di una unità il valore della velocità. Ad ogni pressione del pulsante P2 si decrementa la velocità del motore i una unità. All’avvio di Arduino il motore è spento. Mostrare la velocità impostata sulla serial monitor.

Esercizio 2
Aggiungere all’esercizio precedente un pulsante P3 di emergenza che alla pressione ferma il motore. Se è stata azionata l’emergenza i pulsanti P1 e P2 di incremento e decremento non funzionano. Per poter riattivare il sistema bisogna premere nuovamente P3.

Arduino – Approfondimenti sulla modulazione di larghezza di impulso (PWM)

Scrivo questo post ad integrazione della lezione: Arduino – lezione 06: modulazione di larghezza di impulso (PWM) che sto utilizzando con i miei studenti di 4′ informatica per illustrare le modulazioni di tipo digitali. L’obiettivo è quello di mostrare sull’oscilloscopio come varia il  Duty Cycle di un’onda quadra su un pin di tipo PWM di Arduino utilizzato per impostare l’intensità luminosa di un LED mediante una regolazione applicata attraverso un trimmer connesso al pin A0 di Arduino.

Oltre alla visualizzazione sull’oscilloscopio si desidera, come riscontro, la stampa sulla Serial Monitor dei seguenti valori:

  • Tensione in input sul pin A0
  • Valore restituito dalla funzione analogRead() – (tra 0 e 1023)
  • Valore restituito dall’analogWrite – (tra 0 e 254)
  • Valore percentuale del Duty Cycle  (tra 0% e 100%)

Il circuito da realizzare con l’indicazione delle connessioni all’oscilloscopio è il seguente:

Sul canale X verrà visualizzata l’onda quadra in uscita dal pin 11 il cui Duty Cycle sarà regolato agendo sul trimmer.

Sul canale Y verrà visualizzata la tensione continua in input sul pin A0, che sarà convertita dal convertitore Analogico Digitale di Arduino in un valore compreso tra 0 e 1023  (risoluzione di 10 bit). Ricordo che tale conversione sarà fatta con l’istruzione analogRead(pin).

Poiché uno degli obiettivi è quello di visualizzare la tensione rilevata sul pin A0, ricordo che tale misurazione viene fatta utilizzando la funzione analogRead(pin) che legge il valore di tensione (compreso tra 0 e 5V) applicato sul piedino analogico ‘pin’ con una risoluzione di 10 bit e la converte in un valore numerico compreso tra 0 e 1023, corrispondente quindi ad un intervallo di 1024 valori, pertanto ogni intervallo corrisponde ad un valore di tensione Vu di:

Per sapere quindi il valore di tensione rilevato (nell’intervallo tra 0V e 5V) sarà sufficiente moltiplicare la tensione unitaria Vu per il valore restituito dalla funzione analogRead(pin), valore quantizzato indicato con Vq compreso tra 0 e 1023:

Sapendo che Vu corrisponde a 4,88 mV

possiamo anche scrivere che:

Questa formula sarà inserita all’interno dello sketch.

Di seguito la schermata dell’oscilloscopio che visualizza la situazione indicata dai dati stampati sulla Serial Monitor:

  • Vmax(2) indica la tensione in ingresso ad A0 (la piccola discrepanza tra valore indicato sull’oscilloscopio e la stampa sulla Serial Monitor dipende dalle approssimazioni di calcolo).
  • Vmax(1) indica il valore di picco della tensione sul pin 11.

La spiegazione del funzionamento dello sketch sono dettagliate nei commenti:

/* Prof. Michele Maffucci
   03.06.2019

   Regolazione luminosità LED mediante
   trimmer, si utilizza la funzione map

   Stampa sulla seriale:
   - del valore di tensione sul pin A0
   - del valore restituito dall'analogRead
   - del valore restituito dall'analogWrite
   - del valore del Duty Cycle %

   Questo codice è di dominio pubblico
*/

// pin analogico su cui inviare la tensione analogica (pin A0)
int misura = 0;

// pin a cui è connesso il LED
int pinLed = 11;

// variabile in cui conservare il valore inserito su A0
long val = 0;

// variabile in cui memorizzare il Duty Cycle
int inputVal = 0;

const long VoltRiferimento = 5.0; // valore di riferimento


void setup(){
  Serial.begin(9600);      // inizializzazione della comunicazione seriale
  pinMode(pinLed, OUTPUT); // definizione di ledPin come output
}

void loop(){
  // analogRead leggerà il valore su A0 restituendo un valore tra 0 e 1023
  // per approfondimenti si consulti il link: http://wp.me/p4kwmk-1Qd
  val = analogRead(misura);

  // analogWrite() accetta come secondo parametro (PWM) valori tra 0 e 254
  // pertanto "rimappiamo" i valori letti da analogRead() nell'intervallo
  // tra 0 e 254 usando la funzione map
  // per approfondimenti si consulti il link: http://wp.me/p4kwmk-1Tu
  inputVal = map(val, 0, 1023, 0, 254);

  // accendiamo il LED con un valore del Duty Cycle pari a val
  analogWrite(pinLed,inputVal);

  // Tensione inviata sul pin analogico A0.
  // Valore in virgola mobile.

  float volt = (VoltRiferimento/1024.0)*val;

  // visualizzazione il valore della tensione su A0,
  // del valore restituito dalla analogRead,
  // del valore restituito dall'analogWrite
  // e del Duty Cycle %

  // per approfondimenti sull'uso di String si consulti il link: https://www.arduino.cc/reference/en/language/variables/data-types/stringobject/

  Serial.println(String("Tensione su A0: ") + volt + "V" + String(";  ") + "analogRead: " + val + String(";  ") + String("Valore analogWrite: ") + inputVal + String("; ") + String("Duty Cycle %: ") + (inputVal/255.0)*100 + String("%;"));
  delay(500); // stampa una strina di valori ogni mezzo secondo
}

Buon Coding a tutti 🙂