Archivi tag: programmazione

Arduino – lettura di caratteri da Serial Monitor

La Serial Monitor è la finestra di chat tra noi e la scheda Arduino. Da un lato c’è il computer (con l’IDE o l’editor web), dall’altro c’è il microcontrollore. Quando apriamo la Serial Monitor, stiamo aprendo un canale in cui possiamo leggere messaggi che Arduino ci invia (testi, numeri, stati dei sensori) e scrivergli comandi (caratteri, parole, numeri) per fargli fare cose. È il modo più semplice e immediato per “vedere dentro” un programma che sta funzionando su una scheda senza schermo.

Saper usare la Serial Monitor è utile perché in elettronica e automazione non basta “caricare lo sketch e sperare che funzioni tutto”, bisogna capire cosa succede: quanto misura un sensore, se un calcolo è corretto, se un evento si verifica davvero. Con Serial.print() e Serial.println() facciamo in modo che Arduino ci spieghi passo passo cosa sta facendo. Questo è fondamentale per il debug: quando qualcosa non funziona, far “parlare” il codice è spesso la via più rapida per scoprire l’errore.

Concetti chiave

  • Velocità (baud rate): indica il numero di caratteri al secondo che vengono trasmessi. Questa velocità deve coincidere tra sketch e Serial Monitor, altrimenti ricevete  caratteri non corretti. Usare Serial.begin(9600) o altre velocità tipiche (9600/115200).
  • Buffer di ricezione: i byte arrivano e restano nel buffer finché non li leggete. Serial.available() ti dice quanti.
  • Terminatori di riga nel Serial Monitor: potete inviare No line ending (Nessun fine riga), Newline (\n) (A capo (NL)), Carriage return (\r) (Ritorno carrello (CR)), Both NL & CR (Entrambi NL & CR). Scegliendo A campo (NL) potete leggere “una riga per volta”.

Durante le precedenti lezioni di ripasso abbiamo visto in più occasioni come usare la Serial Monitor, quindi questa che state leggendo è una lezione di ripasso, di seguito i link alle lezioni precedenti:

Riporto di seguito una serie di programmi standard che possono essere riutilizzati in più occasioni:

Esempio 01: echo con conteggio: leggo caratteri subito (non bloccante)

// Prof. Maffucci Michele
// data: 11.11.25
// Fare "echo" dei caratteri ricevuti e contare quanti ne vengono letti.
// Impostazioni Serial Monitor: qualsiasi line ending (anche nessuno va bene).

unsigned long totale_byte = 0;

void setup() {
  Serial.begin(115200); // per Arduino R4, per R3 impostare 9600
  while (!Serial) {}    // necessario su alcune schede USB native
  Serial.println("Echo pronto: digita qualcosa...");
}

void loop() {
  while (Serial.available() > 0) {     // ciclo finché c'è input
    int c = Serial.read();             // leggo un byte
    totale_byte++;
    Serial.write(c);                   // rimando lo stesso byte (echo)
  }

  // Ogni 2 secondi stampo lo stato sulla Serial Monitor
  static unsigned long t0 = 0;
  if (millis() - t0 > 2000) {
    Serial.print("\nByte letti finora: ");
    Serial.println(totale_byte);
    t0 = millis();
  }
}

Dovreste notare che se avete impostato “Entrambi NL &CR” e digitate ad esempio A e premete invio il conteggio dei byte viene fatto in questo modo:

  • A > 1 byte
  • \r > Carriage Return > 1 byte
  • \n > Newline > 1 byte

totale = 3 byte.

Se impostate “A capo (NL)” otterrete un totale di 2 byte (A + \n).
Con “Nessuna fine riga” ottenete 1 byte (solo A).

Impostazione Monitor Cosa viene inviato dopo il carattere Byte totali per tasto
No line ending (nulla) 1
Newline \n 2
Carriage return \r 2
Both NL & CR \r\n 3

Esempio 02: lettura “riga per riga” con \n (line ending = Newline – “A capo(NL))

// Prof. Maffucci Michele
// data: 11.11.25
// Leggere comandi testuali una riga alla volta (terminatore '\n').
// Impostazioni Serial Monitor: "Newline" come line ending.

// DIM = dimensione massima del buffer di input, inclusa la chiusura '\0'.
// Sceglietela abbastanza grande da contenere la riga più lunga che prevedete.
const byte DIM = 64;

// riga[] è un array di char (buffer) dove accumuliamo i caratteri ricevuti
// fino all'invio '\n'. Essendo "C string", va CHIUSA con '\0' quando completa.
char riga[DIM];

// i è l'indice di scrittura corrente dentro riga[] (prossima posizione libera).
// Parte da 0 e cresce a ogni carattere valido letto; non deve mai superare DIM-1.
byte i = 0;

void setup() {
  Serial.begin(115200); // per Arduino Uno R4, per R3 impostare 9600
  while (!Serial) {}
  Serial.println("Inserisci un comando (LED_ON / LED_OFF / HELP) e premi Invio");
}

void loop() {
  while (Serial.available() > 0) {
    char c = (char)Serial.read();

    // Se il Monitor è su "Both NL & CR", scarto il ritorno carrello '\r'
    if (c == '\r') continue;

    if (c == '\n') {            // RIGA COMPLETA
      riga[i] = '\0';           // Chiudo la stringa C (terminatore)
      eseguiComando(riga);      // Passo l'intera riga alla funzione di parsing
      i = 0;                    // Reset per la prossima riga
    } else if (i < DIM - 1) { // Protezione da overflow: lascio 1 posto per '\0' riga[i++] = c; // Accumulo il carattere e avanzo l'indice } // Se il buffer è pieno, i caratteri extra vengono ignorati } } void eseguiComando(char* cmd) { // strcmp(a,b) confronta due stringhe C (char*) e restituisce: // 0 --> uguali
  //  <0 --> a < b // >0  --> a > b
  // ricordare: è case-sensitive ("LED_ON" != "led_on").

  if (strcmp(cmd, "LED_ON") == 0) {   // esattamente "LED_ON"?
    Serial.println("Accendo (simulato) LED");
  } else if (strcmp(cmd, "LED_OFF") == 0) {
    Serial.println("Spengo (simulato) LED");
  } else if (strcmp(cmd, "HELP") == 0) {
    Serial.println("Comandi validi: LED_ON, LED_OFF, HELP");
  } else {
    Serial.print("Comando sconosciuto: ");
    Serial.println(cmd);
  }
}

ricordo che strcmp(...)

  • confronta contenuti delle C-string fino al primo ‘\0’
  • ritorna 0 quando le stringhe sono identiche (è il caso che usiamo negli if)
  • è case-sensitive: “LED_ON” è diverso da “led_on”
  • per confronti su prefisso: strncmp(cmd, “PWM:”, 4) == 0
  • ricorda di passargli stringhe chiuse con ‘\0’ (come facciamo con riga[i] = ‘\0’;)

Ho parlato su queste pagine in più occasioni degli array (seguite il link) un ripasso veloce:

  • un array è una sequenza contigua di celle tutte dello stesso tipo, indicizzate da 0 a N-1;
  • un char buf[N] è spesso usato come stringa C: la parte “valida” termina con '\0' (byte zero);
  • mai uscire dai limiti: per una stringa lunga al massimo N-1, lascia sempre l’ultimo posto per '\0';
  • accesso in scrittura: mantenere un indice (es. i) e verifica i <N-1 prima di scrivere e poi chiudere con buf[i]='\0';
  • per evitare “sporcizia” residua, non serve azzerare tutto il buffer ogni volta: basta chiudere correttamente la stringa;
  • per copiare in sicurezza: strncpy(dst, src, N-1); dst[N-1] = '\0';
  • se preferite evitare la gestione manuale di '\0', valutate String di Arduino; però per prestazioni/memoria prevedibili (e in contesti embedded reali) gli array di char restano la scelta più affidabile.

Esempio 03: lettura “fino a terminatore” con blocco controllato

// Prof. Maffucci Michele
// data: 11.11.25
// Lettura "fino a terminatore" con blocco controllato
// Leggere una sequenza di caratteri dall’utente fino a un terminatore (es. '#')
// usando funzioni bloccanti ma con un timeout breve, così la loop resta reattiva.

const byte DIM = 32;    // dimensione massima del buffer, compreso lo '\0' finale
const char TERM = '#';  // carattere terminatore scelto per “chiudere” l’input

char buf[DIM];  // buffer dove accumulo i caratteri ricevuti

void setup() {
  Serial.begin(115200);
  while (!Serial) {}       // su schede con USB nativa: attendo l’apertura della porta
  Serial.setTimeout(100);  // timeout per tutte le funzioni bloccanti Serial (100 ms)
                           // -> se l’utente NON digita nulla, la readBytesUntil attenderà
                           //    al massimo 100 ms e poi restituirà 0 byte letti
  Serial.println("Digita testo e termina con '#' (terminatore).");
  Serial.println("Suggerimento: imposta il line ending su 'No line ending'.");
}

void loop() {
  // readBytesUntil BLOCCA fino a quando:
  //  - incontra il terminatore (TERM), oppure
  //  - raggiunge (DIM-1) caratteri (lasciamo 1 posto per '\0'), oppure
  //  - scade il timeout impostato con setTimeout(...)
  //
  // Ritorna quanti byte REALMENTE copiati in buf (senza includere il terminatore).
  size_t n = Serial.readBytesUntil(TERM, buf, DIM - 1);

  if (n > 0) {      // ho letto qualcosa (terminatore trovato o buffer quasi pieno)
    buf[n] = '\0';  // chiudo la C-string

    // Se nel Serial Monitor usate "Both NL & CR" ed inviate per errore \r o \n
    // dentro al testo, potreste voler ripulire la stringa:
    // stripCRLF(buf);

    Serial.print("Ricevuto (");
    Serial.print(n);
    Serial.print(" char): ");
    Serial.println(buf);

    // Esempio: gestisco un comando speciale
    if (strcmp(buf, "RESET") == 0) {  // strcmp ritorna 0 se le stringhe sono identiche
      Serial.println("-> Comando RESET eseguito (simulato).");
    } else if (strncmp(buf, "PWM:", 4) == 0) {  // prefisso "PWM:" seguito da valore

      // "buf" contiene l'intera riga ricevuta, es. "PWM:128"
      // Confrontato il prefisso "PWM:", vogliamo estrarre solo la parte numerica ("128").
      // "buf + 4" significa: punta al 5° carattere della stringa (salta 'P','W','M',':').
      // atoi(...) converte la sottostringa numerica in int (qui: 128).

      int v = atoi(buf + 4);
      if (v >= 0 && v <= 255) { Serial.print("-> Imposto PWM a ");
        Serial.println(v);
      } else {
        Serial.println("-> Valore PWM fuori range (0..255).");
      }
    } else {
      Serial.println("-> Testo generico ricevuto.");
    }

    // readBytesUntil CONSUMA il terminatore dal flusso,
    // quindi non dovete "buttarlo" voi. Se invece usate readBytes (senza Until),
    // il terminatore resterebbe nel buffer e dovreste occuparvene manualmente.
  }

  // Se n == 0, non ho letto nulla: o non è arrivato niente entro 100 ms,
  // o l'utente ha inviato meno caratteri del previsto e non ha terminato con '#'.
  // In ogni caso il loop rimane libero di fare altro.
}

setTimeout() controlla l’attesa massima delle funzioni bloccanti come readBytesUntil(), readString(), parseInt() ecc.

Esempio 04: Lettura di numeri con parseInt() e validazioni

// Prof. Maffucci Michele
// data: 11.11.25
// Acquisire un numero intero inserito dall'utente, con controllo di validità.
// Impostazioni: line ending opzionale; consigliato "A capo(NL)".

void setup() {
  Serial.begin(115200); // per Arduino Uno R4, per R3 impostare 9600
  while (!Serial) {}
  Serial.setTimeout(3000);  // 3 s per digitare il numero
  Serial.println("Inserisci un numero intero tra 0 e 255 e premi Invio:");
}

void loop() {
  if (Serial.available() > 0) {
    long val = Serial.parseInt();  // blocca finché trova cifre o va in timeout
    if (val >= 0 && val <= 255) { Serial.print("OK, hai scritto: "); Serial.println(val); } else { Serial.println("Valore non valido o fuori range (0..255). Riprova."); } // Svuoto eventuali residui sulla linea while (Serial.available() > 0) {
      Serial.read();
    }
    Serial.println("Inserisci un nuovo numero:");
  }
}

parseInt() e parseFloat() saltano caratteri non numerici, leggono il numero e rispettano setTimeout().

Esempio 05: parser a stati non bloccante

// Prof. Maffucci Michele
// data: 11.11.25
// Parser “a stato” senza blocchi
// Leggere comandi testuali dalla Serial senza usare funzioni bloccanti.
// Formati accettati (esempi):
//   - "S1\n"   -> LED ON
//   - "S0\n"   -> LED OFF
//   - "PWM:123\n" -> imposta duty a 123
//
// Strategia:
//  - Usiamo una MACCHINA A STATI: IN_ATTESA -> IN_TOKEN -> (':' visto?) IN_VALORE -> (newline) ESEGUI
//  - Leggiamo byte con available()/read() (mai bloccare).
//  - Accumuliamo in due buffer separati: token (comando) e valore (parametro).
//  - Al newline '\n' consideriamo il comando “completo” e lo eseguiamo.
//  - Ignoriamo '\r' (utile se il Monitor invia "Both NL & CR").
//
// Vantaggi: loop reattiva (puoi fare sensori/attuatori mentre leggi), nessun timeout da gestire.

enum StatoInput {      // Enum = elenco di costanti intere leggibili che rappresentano gli stati possibili
  IN_ATTESA,           // Non sto leggendo nulla (skip spazi, attendo primo char utile del token)
  IN_TOKEN,            // Sto leggendo il nome del comando (es. "S1" o "PWM")
  IN_VALORE            // Ho visto ':' e sto leggendo il valore (es. "123")
};
// Nota: di default, IN_ATTESA=0, IN_TOKEN=1, IN_VALORE=2 (interi consecutivi).

StatoInput stato = IN_ATTESA;   // stato corrente della macchina a stati

// Dimensioni dei buffer: lascia sempre 1 char per il terminatore '\0'
const byte DIM_TOKEN = 8;
const byte DIM_VAL   = 8;

char token[DIM_TOKEN];          // buffer per il comando (senza parametri)
char valore[DIM_VAL];           // buffer per il parametro (se presente)
byte iT = 0, iV = 0;            // indici di scrittura nei buffer

// (Opzionale) Normalizza in minuscolo: utile se vuoi comandi case-insensitive
char toLowerChar(char c) {
  if (c >= 'A' && c <= 'Z') return c + ('a' - 'A');
  return c;
}

void setup() {
  Serial.begin(115200);
  while (!Serial) {}
  Serial.println("Comandi: S0, S1, PWM:<0..255>  (termina con Invio)");
  Serial.println("Esempi: S1\\n   S0\\n   PWM:128\\n");
}

void loop() {
  // ---- QUI PUOI FARE ALTRO (letture sensori, logica, attuatori) ----
  // NIENTE nel parser sotto blocca l'esecuzione.

  while (Serial.available() > 0) {
    char c = (char)Serial.read();     // prendo 1 byte dal buffer RX
    if (c == '\r') continue;          // ignoro CR (se Monitor manda Both NL & CR)

    if (c == '\n') {                  // newline = riga/ comando completo
      token[iT]  = '\0';              // chiudi stringhe C
      valore[iV] = '\0';

      esegui(token, valore);          // interpreta ed esegui

      // reset parser per prossima riga
      stato = IN_ATTESA;
      iT = iV = 0;
      continue;
    }

    // Se non è newline, gestisco in base allo stato
    switch (stato) {

      case IN_ATTESA:
        // Salto eventuali spazi iniziali, poi inizio a costruire il token
        if (c == ' ' || c == '\t') {
          // rimango in attesa
        } else {
          stato = IN_TOKEN;           // ho trovato il primo char "utile"
          if (iT < DIM_TOKEN - 1) {
            token[iT++] = c;          // salvo nel token
          }
          // se pieno, i char extra verranno scartati (vedi controllo nei case seguenti)
        }
        break;

      case IN_TOKEN:
        if (c == ':') {
          // Due casi:
          // - Comandi con parametro (es. "PWM:123"): passo allo stato IN_VALORE
          // - Comandi senza parametro ma con ':' accidentale: comunque vado in IN_VALORE (sarà vuoto)
          stato = IN_VALORE;
        } else if (c == ' ' || c == '\t') {
          // Ignoro spazi in mezzo al token (scelta di design: potresti anche “chiudere” il token qui)
        } else {
          // Accumulo altri caratteri del token se c'è spazio
          if (iT < DIM_TOKEN - 1) token[iT++] = c;
          // Se il token supera la capienza, i char extra vengono silenziosamente scartati
        }
        break;

      case IN_VALORE:
        // Tutto ciò che arriva fino al newline è il valore (spazi inclusi)
        if (iV < DIM_VAL - 1) valore[iV++] = c;
        // Se supera la capienza, scarto il resto del valore
        break;
    }
  }
}

// Esegue il comando interpretando token e valore
void esegui(const char* tk, const char* val) {
  // --- Esempio: comandi "S1" e "S0" (LED simulato) ---
  // strcmp ritorna 0 quando le stringhe sono identiche (case-sensitive).
  if (strcmp(tk, "S1") == 0) {
    Serial.println("LED = ON (simulato)");
    return;
  }
  if (strcmp(tk, "S0") == 0) {
    Serial.println("LED = OFF (simulato)");
    return;
  }

  // --- Esempio: comando con parametro "PWM:<0..255>" ---
  // Confronto solo il prefisso "PWM" (3 caratteri). Se uguale, mi aspetto il valore in 'val'.
  if (strncmp(tk, "PWM", 3) == 0) {
    // Converte il valore da stringa a intero.
    // Nota: atoi non segnala errori; in casi reali preferisci controllare che 'val' contenga solo cifre.
    int duty = atoi(val);
    if (duty >= 0 && duty <= 255) {
      Serial.print("Imposto PWM a ");
      Serial.println(duty);
    } else {
      Serial.println("PWM fuori range (0..255).");
    }
    return;
  }

  // --- Default: comando sconosciuto ---
  Serial.print("Comando sconosciuto: '");
  Serial.print(tk);
  Serial.print("'");
  if (val[0] != '\0') {
    Serial.print(" con valore '");
    Serial.print(val);
    Serial.print("'");
  }
  Serial.println();
}

Funzionamento:

  1. Accumulo byte finché ce n’è (available()>0).
  2. Transizioni di stato:
    • IN_ATTESA > ignora spazi > primo carattere utile > IN_TOKEN
    • IN_TOKEN > caratteri “nome comando”; se arriva ':' > IN_VALORE
    • IN_VALORE > accumulo fino al newline
  3. Newline \n = “fine comando”: chiudo le stringhe con '\0' e chiamo esegui(...)
  4. protezione buffer: sempre DIM-1 per lasciare spazio al '\0'. Char extra > scartati
  5. niente blocchi: nessun readString/parse*; la loop() resta fluida.

Non ricordo se avevo già parlato di enum ma vi scrivo qualcosina.

Un enum elenca costanti intere con nomi leggibili, esempio:

enum StatoInput { IN_ATTESA, IN_TOKEN, IN_VALORE };

equivale (di default) a: IN_ATTESA=0, IN_TOKEN=1, IN_VALORE=2.

Usarlo in una macchina a stati è utile perché:

  • leggibilità: stato = IN_TOKEN; è auto-esplicativo
  • sicurezza: eviti “numeri di significato ambiguo” (0,1,2) sparsi nel codice
  • aiuta con switch (stato) { … }

Tipo sottostante: in C/C++ classico l’enum usa un intero (di solito int); su AVR è tipicamente int.
In C++11 esiste anche enum class (scoped enum), con scope ristretto e nessuna conversione implicita a int:

enum class StatoInput { InAttesa, InToken, InValore };
StatoInput s = StatoInput::InAttesa;
// switch(s) { case StatoInput::InToken: ... }

Qualche NOTA importante a questa lezione, dedicato ai:

  1. “lamentosi”
  2. studenti e non, bravi o svogliati
  3. a chi si dimentica che ciò che scrivo è gratuito ed in primis è rivolto ai miei studenti che hanno la priorità
  4. alle persone che fanno parte della categoria dei “flamer” o dei “troll” quindi per me identificabili come “ciucci” e “perdi tempo a tradimento” (come diceva un mio insegnante)

dico:

  1. è stato abbastanza faticoso scrivere questa lezione, pertanto non troverete le consuete dimostrazioni animate alla fine di ogni sketch
  2. copiate ed incollate e verificate se non ci sono errori, non dovrebbero essercene, ma la stanchezza può averne aggiunto qualcuno… consiglio: fate dell’errore un motivo di miglioramento
  3. se notate qualche formattazione del codice di esempio (codice tutto su un riga) ciò è imputabile a qualche carattere precedente nello sketch che aggiunge un errore di formattazione, poiché ancora non ho trovato soluzione, fate “copia ed incolla” nell’IDE Arduino ed una successiva “Formattazione automatica” dal menù strumenti questo aggiusta tutto
  4. non assegnerò come di consueto gli esercizi finali per due motivi:
    • credo che sia già un esercizio capire il funzionamento degli sketch (per me è stato un bel ripasso) 🙂
    • vedi punto 2 (stanchezza)

Buon studio a tutti 🙂

Arduino – istruzione “do…while” – soluzione esercizi proposti

In riferimento alla lezione: Arduino – istruzione “do…while”: eseguire almeno una volta, poi verificare pubblico una possibile soluzione agli esercizi proposti.

Esercizio 01 – Attendi pulsante

  • Consegna: attendere che un pulsante su D2 venga premuto; durante l’attesa far lampeggiare il LED integrato.
  • Vincoli: usare while come attesa in ingresso (while(digitalRead(…)==HIGH)).
  • Extra: al termine, stampare “OK” e fermarsi.

Soluzione

/*
  Prof. Maffucci Michele
  data: 27.10.25

  Soluzione esercizio 01 - Attendi pulsante
  Hardware:
    - Pulsante su D2 con INPUT_PULLUP (un capo a D2, l'altro a GND)
    - LED integrato (pin 13)
  Obiettivo:
    - Lampeggiare il LED mentre si attende la pressione del pulsante
    - Usare while(digitalRead(PIN_BTN) == HIGH) come attesa
    - Quando il pulsante viene premuto -> stampare "OK" e fermarsi
*/

// PIN_BTN dichiarato come byte e non come int per occupare meno spazio di memoria
// collegare il pulsante a GND (INPUT_PULLUP)

const byte PIN_BTN = 2;

void setup() {
  pinMode(LED_BUILTIN, OUTPUT);
  pinMode(PIN_BTN, INPUT_PULLUP);
  Serial.begin(9600);
  Serial.println("Premi il pulsante per continuare...");
}

void loop() {
  // Attesa attiva: finché il pulsante NON è premuto (HIGH con pull-up)
  // il LED_BUILTIN (13) lampeggia
  while (digitalRead(PIN_BTN) == HIGH) {
    digitalWrite(LED_BUILTIN, HIGH);
    delay(150);
    digitalWrite(LED_BUILTIN, LOW);
    delay(150);
  }

  // Debounce semplice per elimina rimbalzi meccanici
  // realizzato con piccolo ritardo
  delay(30);
  
  while (digitalRead(PIN_BTN) == LOW) {  // resta qui finché è tenuto premuto
  }

  Serial.println("OK");
  digitalWrite(LED_BUILTIN, HIGH);      // lascia il LED acceso come conferma
  while (true) {}                       // ferma la demo
}

Esercizio 02 – Svuota buffer seriale

  • Consegna: quando l’utente invia testo, fare echo e svuotare tutti i caratteri residui.
  • Vincoli: usare while(Serial.available()>0).
  • Extra: contare quanti byte sono stati letti e mostrarli.

Soluzione

/*
  Prof. Maffucci Michele
  data: 27.10.25

  Soluzione esercizio 02 — Svuota buffer seriale
  Obiettivo:
    - Appena arriva del testo, fare echo di TUTTI i caratteri presenti nel buffer
    - Usare while(Serial.available() > 0) per svuotare il buffer
    - Contare i byte letti e mostrarli
  Ricordare:
    - Impostare il Serial Monitor a 9600 baud
*/

void setup() {
  Serial.begin(9600);
  Serial.println("Scrivi una riga di testo e premi invio...");
}

void loop() {
  if (Serial.available() > 0) {
    int conteggioByte = 0;

    Serial.print("Echo: ");
    // Legge TUTTO ciò che è attualmente in buffer
    while (Serial.available() > 0) {
      char c = Serial.read();     // leggi un byte
      Serial.write(c);            // echo (stampa il carattere così com'è)
      conteggioByte++;            // conta i byte letti
      // piccola attesa facoltativa per dare tempo al buffer di riempirsi
      delay(1);
    }
    Serial.println();
    Serial.print("Byte letti: ");
    Serial.println(conteggioByte);
    Serial.println("----");
  }
  // Se serve introdurre qui altro codice non bloccante
}

Attenzione!

Può capitare che vengano contati due byte in più, ciò accade perché la Serial Monitor (in basso a destra)
aggiunge automaticamente i caratteri di fine riga quando premete invio.

  • entrambi NL & CR (Both NL & CR) > aggiunge due byte: \r (CR, 13) e \n (LF, 10) → ecco il tuo +2 costante;
  • a capo NL (Newline) > aggiunge un byte: \n > +1;
  • ritorno carrello (CR) (Carriage return) > aggiunge un byte: \r > +1;
  • nessuna fine riga (No line ending) > +0 (il conteggio coincide con i caratteri digitati, salvo caratteri non ASCII).

Esercizio 03 – Timer regressivo

  • Consegna: da un valore t letto da Serial (es. 5..20), eseguire un countdown finché t>0.
  • Vincoli: usare while(t>0) con t– e stampa del tempo.
  • Extra: beep finale + messaggio “Decollo!”.

Soluzione

/*
  Prof. Maffucci Michele
  data: 27.10.25

  Soluzione esercizio 03 — Timer regressivo
  Obiettivo:
    - Leggere un intero t (5..20) dal Serial Monitor
    - Eseguire while(t > 0) con stampa del tempo e t--
    - Al termine: beep su D5 e messaggio "Decollo!"
  Note:
    - Usa Serial.parseInt() per semplicità (attende un numero)
    - Per maggior informazioni sull'uso di parseInt() consultare il link:
      https://wp.me/p4kwmk-4Ah
*/

const int PIN_BUZ = 5;  // collegare un buzzer piezo (se disponibile)

void setup() {
  Serial.begin(9600);
  pinMode(PIN_BUZ, OUTPUT);
  Serial.println("Inserisci un numero intero t tra 5 e 20 e premi invio:");
}

void loop() {
  int t = 0;

  // Attendi un numero valido nell'intervallo [5..20]
  while (t < 5 || t > 20) {
    if (Serial.available() > 0) {
      t = Serial.parseInt();  // legge il primo intero disponibile
      // Svuota residui (ad es. '\n')
      while (Serial.available() > 0) Serial.read();

      if (t < 5 || t > 20) {
        Serial.println("Valore non valido. Inserisci un numero tra 5 e 20:");
      }
    }
  }

  // Countdown con while(t > 0)
  while (t > 0) {
    Serial.print("T-");
    Serial.println(t);
    delay(1000);  // 1 secondo
    t--;
  }

  // Fine: beep + messaggio
  tone(PIN_BUZ, 880, 350);  // beep di 350 ms
  Serial.println("Decollo!");

  while (true) {}  // ferma la demo
}

Buon lavoro.

Progettare bene, programmare meglio: pseudocodice e diagrammi per sistemi elettronici – lezione 5/5


Ripasso di inizio anno – appunti per la classe.

Di seguito un mini vocabolario, come sempre stampatelo e mettetelo nel tuo quadernone (alcune cose le abbiamo viste anche nella lezione precedente):

Useremo un dialetto semplice.

  • Una istruzione per riga.
  • Parole chiave in MAIUSCOLO e con i due punti : quando aprono un blocco:
    • SE … ALLORA: … ALTRIMENTI:
    • MENTRE (condizione):
    • PER i da A a B:
    • FUNZIONE nome(parametri): RITORNA valore
  • Indentazione (rientro) di 2–4 spazi per il contenuto dei blocchi.
  • Commenti: usa // all’inizio della riga.
  • Nomi chiari per variabili e costanti: sogliaLuce, tempoGiallo, pinLed.

Mini-vocabolario

  • IMPOSTA x a 10: assegna un valore
  • LEGGI x: prendi un input (da Serial o da sensore)
  • SCRIVI …: stampa (su Serial o su un display)
  • ATTENDI 500 ms: pausa
  • ESCI DAL CICLO: interrompi MENTRE o PER
  • OperatorI: ==, !=, <, >, >=, <=, AND, OR, NOT

Mappa mentale

Pseudocodice Arduino (C/C++)
IMPOSTA x a 10 int x = 10; oppure x = 10;
LEGGI da Serial parseInt()/readStringUntil('\n')
LEGGI digitale pin digitalRead(pin)
LEGGI analogico pin analogRead(pin)
SCRIVI su Serial Serial.print()/println()
ACCENDI LED digitalWrite(pin, HIGH)
SPEGNI LED digitalWrite(pin, LOW)
ATTENDI t ms delay(t)
PER i da 1 a N: for (int i=1; i<=N; i++) { ... }
MENTRE (condizione): while (condizione) { ... }
SE / ALTRIMENTI if (...) { ... } else { ... }
FUNZIONE tipo nome(params){...}
RITORNA return

Funzioni di aiuto per leggere dal Monitor Seriale

Blocchi riutilizzabili

// ---------------------------
// Aiuti per I/O seriale
// ---------------------------

// Legge un intero dalla Serial (es.: 42, -17).
// Attende che ci siano dati disponibili, usa parseInt()
// e poi "pulisce" il buffer fino al newline '\n' per preparare
// la prossima lettura.
int leggiInt() {
  // Attende finché il buffer seriale è vuoto.
  // Serial.available() restituisce quanti byte sono pronti da leggere.
  while (!Serial.available()) {
    // ciclo di attesa attivo: resta qui finché non arriva almeno 1 byte
  }

  // parseInt():
  //  - salta automaticamente spazi e caratteri non numerici iniziali;
  //  - legge opzionalmente il segno (-);
  //  - si ferma quando incontra un separatore (es. spazio, '\n', ecc.)
  //  - rispetta Serial.setTimeout(): se i numeri arrivano "a pezzi", attende fino al timeout.
  int v = Serial.parseInt();

  // Dopo parseInt possono essere rimasti caratteri nel buffer
  // (ad es. l'utente ha premuto Invio, quindi c'è '\n' o anche "\r\n").
  // Questo while consuma tutto ciò che c'è fino a trovare '\n' e poi esce,
  // in modo da "allineare" la prossima lettura all'inizio della riga seguente.
  while (Serial.available()) {
    // Legge un carattere dal buffer
    if (Serial.read() == '\n') break; // esce quando trova il newline
    // Nota: se volessi trattare anche '\r' come fine riga su sistemi Windows,
    // potresti controllare anche '\r' (carriage return).
  }

  // Restituisce il valore intero letto.
  return v;
}

// Legge una riga di testo fino al newline '\n' e la restituisce come String.
// Esempio di input: "ciao mondo\n" -> "ciao mondo" (senza newline né spazi finali).
String leggiLinea() {
  // Attende che arrivi almeno un byte.
  while (!Serial.available()) {
    // attesa attiva
  }

  // Legge i caratteri fino al delimitatore '\n' (newline).
  // Il newline non viene incluso nella String risultante.
  String s = Serial.readStringUntil('\n');

  // trim():
  //  - rimuove spazi iniziali/finali, tab e ritorni a capo residui.
  //  - Utile se su alcune piattaforme l'invio produce "\r\n" (Windows):
  //    readStringUntil('\n') rimuove '\n', ma può restare '\r' in coda; trim() lo elimina.
  s.trim();

  // Restituisce la riga "pulita".
  return s;
}

Codice senza commenti da copiare ed incollare:

// ——— Aiuti per I/O seriale ———
int leggiInt() {
  while (!Serial.available()) {}
  int v = Serial.parseInt();
  while (Serial.available()) {
    if (Serial.read() == '\n') break;
  }
  return v;
}

String leggiLinea() {
  while (!Serial.available()) {}
  String s = Serial.readStringUntil('\n');
  s.trim();
  return s;
}

Esempi guidati pseudocodice > sketch Arduino

Esempio 01 – Saluto con nome (solo Serial)

Scopo: input/output base, variabili stringa.

Pseudocodice – esempio 01

SCRIVI "Come ti chiami?"
LEGGI nome
SCRIVI "Ciao, " + nome + "!"

Arduino – esempio 01 – con commenti

void setup() {
  Serial.begin(9600);          // Inizializza la seriale a 9600 baud (Monitor Seriale deve avere lo stesso valore).

  while (!Serial) { ; }        // Attende che la porta seriale sia pronta (utile su schede con USB "native", es. UNO R4).
                               // Su UNO R3 spesso non serve, ma non fa danni.

  Serial.println("Come ti chiami?"); // Messaggio iniziale: l’utente vede la richiesta nel Monitor Seriale.

  Serial.setTimeout(60000);    // Imposta il "tempo massimo di attesa" a 60 s per funzioni come readStringUntil/parseInt.
                               // Se l’utente impiega tempo a digitare, la lettura aspetta fino a 60 s prima di rinunciare.
}

void loop() {
  String nome = leggiLinea();  // Legge una riga di testo inserita dall’utente fino al tasto Invio (newline '\n').
                               // Esempio: se scrivo "Michele" e premo Invio, nome = "Michele".

  Serial.print("Ciao, ");      // Stampa senza andare a capo...
  Serial.print(nome);          // ...stampa il nome letto...
  Serial.println("!");         // ...e chiude la riga con il punto esclamativo + newline.

  while (true) {}              // Ferma il programma qui (loop infinito).
                               // Serve per NON chiedere nuovamente il nome in un ciclo senza fine.
                               // Se vuoi che ripeta la domanda, rimuovi questa riga.
}

Arduino – esempio 01 – senza commenti

void setup() {
  Serial.begin(9600);
  while (!Serial) {;}
  Serial.println("Come ti chiami?");
  Serial.setTimeout(60000);
}

void loop() {
  String nome = leggiLinea();
  Serial.print("Ciao, ");
  Serial.print(nome);
  Serial.println("!");
  while (true) {}  // fine
}

Esercizio aggiuntivo: chiedi anche l’età e rispondi “Sei maggiorenne/minorenne”.

Esempio 02 – Blink (LED lampeggiante)

Collegamenti: LED con resistenza su pin 13 (o usa quello onboard).

Pseudocodice – esempio 02

IMPOSTA pinLed a 13
IMPOSTA periodo a 500 ms
MENTRE (vero):
    ACCENDI LED
    ATTENDI periodo
    SPEGNI LED
    ATTENDI periodo

Arduino – esempio 02

const int pinLed = 13;
const int periodo = 500;

void setup() {
  pinMode(pinLed, OUTPUT);
}

void loop() {
  digitalWrite(pinLed, HIGH);
  delay(periodo);
  digitalWrite(pinLed, LOW);
  delay(periodo);
}

Esercizio aggiuntivo: leggi periodo da Serial.

Esempio 03 – Pulsante accende/spegne LED

Collegamenti: pulsante su pin 2, modalità INPUT_PULLUP; LED su pin 8.
Nota: il pulsante chiude a GND (premuto = LOW).

Pseudocodice – esempio 03

IMPOSTA statoLed a SPENTO
MENTRE (vero):
    LEGGI pulsante
    SE (pulsante premuto) ALLORA:
        ATTENDI 20 ms  // anti-rimbalzo semplice
        SE (ancora premuto) ALLORA:
            inverti statoLed
            applica stato al LED
            ATTENDI rilascio

Arduino – esempio 03

const int pinBtn = 2;
const int pinLed = 8;
bool statoLed = false;

void setup() {
  Serial.begin(9600);
  pinMode(pinBtn, INPUT_PULLUP);
  pinMode(pinLed, OUTPUT);
}

void loop() {
  if (digitalRead(pinBtn) == LOW) {  // premuto
    delay(20);                       // debounce base
    if (digitalRead(pinBtn) == LOW) {
      statoLed = !statoLed;  // toggle
      digitalWrite(pinLed, statoLed ? HIGH : LOW);
      // Attendi rilascio
      while (digitalRead(pinBtn) == LOW) {}
      delay(20);
    }
  }
}

Esercizio aggiuntivo: Estensione: stampa su Serial “ON/OFF” ad ogni pressione.

Esercizi

Esercizio 01 – Potenziometro > luminosità (PWM)

Obiettivo: Regolare mediante potenziometro l’intensità luminosa del LED.
Collegamenti: potenziometro su A0; LED su pin PWM 9.

Esercizio 02 – LED ON per 3 secondi quando premo il pulsante

Obiettivo: premo il pulsante > il LED rimane acceso per 3 secondi > poi si spegne.
Evitare ripetizioni mentre si tiene premuto (debounce base).

Componenti & Collegamenti

  • 1 × LED + 1 × resistenza (220–330 Ω) su pin 8 → GND
  • 1 × pulsante su pin 2 con INPUT_PULLUP (l’altro capo del pulsante a GND)

Esercizio 03 — LDR: accendi il LED quando è buio

Obiettivo: leggo la luce con una LDR. Se il valore è sotto una soglia (buio), accendo il LED; altrimenti lo spengo. Stampo i valori su Serial.

Componenti & Collegamenti

  • LDR in partitore su A0 (esempio: 5V — LDR — A0 — resistenza 10 kΩ — GND)
  • LED + resistenza su pin 9 → GND

Esercizio 04 – Potenziometro > luminosità LED (PWM)

Obiettivo: con il potenziometro regolo la luminosità del LED usando analogWrite (PWM).

Componenti & Collegamenti

  • Potenziometro 10 kΩ su A0 (estremi a 5V e GND, cursore ad A0)
  • LED + resistenza su pin ~9 (pin PWM) > GND

Buon Coding a tutti 🙂

Lezione 11 – Corso di Elettronica Creativa con Arduino Sensor Kit

Accelerometro

L’accelerometro del Sensor Kit è un “sensore di movimento” a tre assi (X, Y, Z) capace di misurare come cambia la velocità di un oggetto nello spazio. Anche quando è fermo, l’accelerometro non rileva “zero accelerazione” in quanto misura la gravità terrestre, circa 1 g (≈ 9,81 m/s²). Questa caratteristica è utilissima perché ci permette sia di capire se e quanto ci stiamo muovendo, sia di stimare l’inclinazione del modulo rispetto alla verticale. In pratica: se appoggio la scheda piatta sul banco, il valore lungo Z sarà vicino a 1 g; se lo incliniamo, la gravità si “ridistribuisce” tra X e Y e possiamo ricavare l’angolo di inclinazione.

Con i dati che fornisce l’accelerometro possiamo fare molte cose: visualizzare scosse e vibrazioni (come un mini sismografo), creare un avviso di caduta, progettare giochi di equilibrio o semplicemente tracciare i movimenti sulla Serial Plotter. In questa lezione impareremo a leggere le tre componenti, a filtrare il rumore e a trasformare quei numeri in azioni concrete (LED, buzzer, messaggi su OLED).

Un briciolo di teoria

L’accelerometro misura accelerazioni lungo X, Y, Z e come dicevo sopra, da fermo “vede” la gravità (~1 g ≈ 9,81 m/s²).

Il modulo del vettore accelerazione è:

Con il dispositivo quasi fermo, possiamo stimare l’inclinazione rispetto alla verticale usando solo la gravità:

che rappresenta l’angolo rispetto all’asse Z (in gradi).

Caratteristiche tecniche (per i più esperti)

  • Tipo sensore: MEMS accelerometro triassiale digitale.
  • Interfacce: I²C e SPI digitali (il modulo Grove è normalmente usato in I²C sul Sensor Kit).
  • Indirizzo I²C: di default 0x19, commutabile a 0x18 (pin SA0/SDO a GND).
  • Campo di misura (full-scale): ±2 g, ±4 g, ±8 g, ±16 g (selezionabile via registro).
  • Risoluzione dati: uscita fino a 16-bit; modalità operative low-power/normal/high-resolution (fino a 12-bit effettivi).
  • Frequenza di campionamento (ODR): da 1 Hz fino a 5.3 kHz (a seconda della modalità).
  • Funzioni integrate:
    • 2 generatori di interrupt programmabili (motion, free-fall, wake-up).
    • Rilevamento orientamento 6D/4D, self-test, FIFO 32 campioni.
  • Alimentazione (chip): 1.71–3.6 V; consumo molto basso (fino a ~2 µA in ultra-low-power). Il modulo Grove è compatibile 3 V / 5 V.
  • Intervallo di temperatura operativa: −40 °C … +85 °C.

Informazioni utili per l’uso nell’Arduino Sensor Kit

  • Con la libreria Arduino_SensorKit.h leggete le tre componenti con Accelerometer.readX() / readY() / readZ(); i valori restituiti sono interpretati in g (gravità ≈ 1 g a riposo).
  • Il modulo Grove usato nel Sensor Kit documenta esplicitamente indirizzi I²C, range e compatibilità 3V/5V, utile se sorgono conflitti su bus I²C o serve cambiare indirizzo.

Collegamenti

Per quanto riguarda i collegamenti è sufficiente connettere il sensore ad uno degli ingressi I²C della Base Shield.

Esempio 01 – Lettura accelerometro 3 assi

/*
  Prof. Maffucci Michele
  02.10.2025
  Lettura accelerometro 3 assi – Arduino Sensor Kit
*/

#include <Arduino_SensorKit.h>   // Libreria unica per i moduli del Sensor Kit

const unsigned long intervalloMs = 500;  // intervallo tra una stampa e la successiva

void setup() {
  // Inizializzazione porta seriale per inviare i dati al PC
  Serial.begin(9600);

  // ATTENZIONE:
  // while(!Serial); serve ad ASPETTARE che il monitor seriale sia stato aperto
  // (utile sulle schede con USB nativa, esempio: Arduino UNO R4 WiFi, Leonardo, MKR).
  // Così non "perdi" le prime stampe.
  // Su schede senza USB nativa non è necessario e puoi rimuoverlo.
  while (!Serial) { ; }

  // Avvio dell'accelerometro a 3 assi
  Accelerometer.begin();
}

void loop() {
  // Lettura delle tre componenti di accelerazione (in g)
  float accelX = Accelerometer.readX();  // Asse X
  float accelY = Accelerometer.readY();  // Asse Y
  float accelZ = Accelerometer.readZ();  // Asse Z

  // Stampa in formato semplice: x:..  y:..  z:..
  Serial.print("x: "); 
  Serial.print(accelX, 3);   // 3 decimali per leggibilità
  Serial.print("  ");

  Serial.print("y: "); 
  Serial.print(accelY, 3);
  Serial.print("  ");

  Serial.print("z: "); 
  Serial.println(accelZ, 3);

  delay(intervalloMs);
}

Esempio 02: Accelerometro – Stampa su Serial Monitor e display OLED

Visualizzazione delle componenti x, y e z dell’accelerazione su Serial Monitor e display OLED

/*
  Prof. Maffucci Michele
  02.10.2025
  Lettura accelerometro 3 assi – Arduino Sensor Kit
  Stampa su Serial Monitor e display Oled
*/

#include <Arduino_SensorKit.h>  // Libreria unica per i moduli del Sensor Kit

const unsigned long intervalloMs = 500;  // intervallo tra una stampa e la successiva

void setup() {
  // Inizializzazione porta seriale per inviare i dati al PC
  Serial.begin(9600);

  // ATTENZIONE:
  // while(!Serial); serve ad ASPETTARE che il monitor seriale sia stato aperto
  // (utile sulle schede con USB nativa, esempio: Arduino UNO R4 WiFi, Leonardo, MKR).
  // Così non "perdi" le prime stampe.
  // Su schede senza USB nativa non è necessario e puoi rimuoverlo.
  while (!Serial) { ; }

  // Avvio dell'accelerometro a 3 assi
  Accelerometer.begin();
  Oled.begin();
  // Inizializza il display OLED.
  // Fa partire la comunicazione I2C e manda al display la sequenza di avvio.
  // Senza questa riga lo schermo non mostra nulla.

  Oled.setFlipMode(true);
  // Ruota il contenuto di 180° (utile se lo vedi capovolto).
  // Se nel tuo caso appare già dritto, puoi mettere false: Oled.setFlipMode(false);

  Oled.setFont(u8x8_font_chroma48medium8_r);
  // Sceglie il font (carattere) 8x8.
  // Con un font 8x8 il display 128x64 si comporta come una griglia 16 colonne × 8 righe.
  // Significa che setCursor(colonna, riga) userà numeri tra 0..15 (colonne) e 0..7 (righe).

  Oled.clear();
  // Pulisce lo schermo (cancella tutto quello che c’era prima).
}

void loop() {
  // Lettura delle tre componenti di accelerazione (in g)
  float accelX = Accelerometer.readX();  // Asse X
  float accelY = Accelerometer.readY();  // Asse Y
  float accelZ = Accelerometer.readZ();  // Asse Z

  // Stampa in formato semplice: x:..  y:..  z:..
  Serial.print("x: ");      // stampa la stringa x: su Serial Monitor
  Serial.print(accelX, 3);  // 3 decimali per leggibilità
  Serial.print("  ");

  Serial.print("y: ");      // stampa la stringa y: su Serial Monitor
  Serial.print(accelY, 3);  // 3 decimali per leggibilità
  Serial.print("  ");

  Serial.print("z: ");        // stampa la stringa z: su Serial Monitor
  Serial.println(accelZ, 3);  // 3 decimali per leggibilità

  Oled.setCursor(0, 0);           // colonna 0, riga 0
  Oled.println("ACCELERAZIONE");  // stampa la stringa e va a capo

  Oled.setCursor(0, 2);   // colonna 0, riga 2
  Oled.print("x: ");      // stampa la stringa x:
  Oled.print(accelX, 3);  // stampa la componente x dell'accelerazione con 3 cifre decimali

  Oled.setCursor(0, 3);   // colonna 0, riga 3
  Oled.print("y: ");      // stampa la stringa y:
  Oled.print(accelY, 3);  // stampa la componente y dell'accelerazione con 3 cifre decimali
  Oled.refreshDisplay();

  Oled.setCursor(0, 4);   // colonna 0, riga 4
  Oled.print("z: ");      // stampa la stringa z:
  Oled.print(accelZ, 3);  // stampa la componente z dell'accelerazione con 3 cifre decimali
  Oled.refreshDisplay();

  delay(intervalloMs);
}

Esempio 03: Accelerometro – Uscita per Serial Plotter (3 curve: X, Y, Z)

Stampa i tre assi con etichette e tab in un’unica riga per campione, in questo modo l’IDE mostra tre curve (X, Y, Z).

/*
  Prof. Maffucci Michele
  02.10.2025
  Accelerometro – Uscita per Serial Plotter (3 curve: X, Y, Z)

  Nota: una riga = un campione; valori etichettati e separati da TAB -> 3 curve nel Plotter.
*/

#include <Arduino_SensorKit.h&gt

const unsigned long periodoCampionamentoMs = 10;  // circa 100 Hz (riduci/aumenta a piacere)

void setup() {
  Serial.begin(115200);
  // Attende l'apertura del Monitor/Plotter Seriali sulle schede con USB nativa
  while (!Serial) { ; }

  Accelerometer.begin();

  // Messaggio di avvio (facoltativo)
  Serial.println("Traccio X,Y,Z (g) - dati GREZZI (nessun filtro)");
}

void loop() {
  // Letture grezze in g
  float x = Accelerometer.readX();
  float y = Accelerometer.readY();
  float z = Accelerometer.readZ();

  // Stampa “label:valore” separati da TAB → il Plotter disegna tre curve
  Serial.print("X:");
  Serial.print(x, 3);
  Serial.print('\t');
  Serial.print("Y:");
  Serial.print(y, 3);
  Serial.print('\t');
  Serial.print("Z:");
  Serial.println(z, 3);

  delay(periodoCampionamentoMs);
}

Versione per esperti

Esempio 04: Accelerometro – Uscita per Serial Plotter (3 curve: X, Y, Z) con filtro passa-basso

Come avete potuto notare nella versione precedente le curve delle tre componenti dell’accelerazione erano seghettate e venivano rappresentate anche piccoli tremolii.

Per evitare questo problema useremo nello sketch che segue un filtro IIR (passa-basso) che smusserà il rumore.

Quali sono i vantaggi nell’utilizzo di un filtro passa basso:

  • Rumore: piccole variazioni veloci e indesiderate nei dati (tremolio della mano, vibrazioni del banco, disturbi elettrici).
  • Passa-basso: un filtro che lascia passare i cambiamenti lenti (il “trend” reale del movimento) e attenua i cambiamenti veloci (dovuti al rumore).
  • IIR (Infinite Impulse Response): significa che l’uscita del filtro dipende sia dal valore attuale sia dall’uscita precedente. In pratica: il filtro “ricorda” un po’ del passato -> è una media mobile esponenziale.

Formule per ogni asse:

uscita_filtrata(t) = α * ingresso(t) + (1−α) * uscita_filtrata(t−1)

α è un numero tra 0 e 1:

  • α piccolo (es. 0.1) > molto liscio (tanto filtraggio) ma più lento a seguire i cambiamenti.
  • α grande (es. 0.6) > meno liscio (meno filtraggio) ma più reattivo.

In sintesi:
scegliere α in base al compromesso “più liscio vs più rapido”.

/*
  Prof. Maffucci Michele
  02.10.2025
  Accelerometro - Uscita per Serial Plotter (3 curve: X, Y, Z) con filtro IIS (passa-basso)

  Note:
  - Il Serial Plotter IDE riconosce coppie "etichetta:valore" sulla stessa riga,
    separate da TAB. Ogni riga = un campione nel tempo.
*/

#include <Arduino_SensorKit.h&gt

const unsigned long periodoCampionamentoMs = 10;  // circa 100 Hz
const bool usaFiltro = true;                      // metti false se vuoi i dati grezzi
const float alfa = 0.2;                           // 0..1 (più piccolo = più filtrato)

float xF = 0, yF = 0, zF = 0;  // stati del filtro

void setup() {
  Serial.begin(115200);

  // Serve ad attendere l'apertura del Monitor/Plotter Seriali sulle schede con USB nativa,
  // così non perdete le prime stampe (su UNO "classico" puoi toglierlo senza problemi).
  while (!Serial) { ; }

  Accelerometer.begin();

  // (Facoltativo) riga di "header" informale: non necessaria, ma utile a chi guarda il log
  Serial.println("Pronto: traccio X,Y,Z (g) su Serial Plotter");
}

void loop() {
  // Letture in g
  float x = Accelerometer.readX();
  float y = Accelerometer.readY();
  float z = Accelerometer.readZ();

  if (usaFiltro) {
    // Filtro IIR (passa-basso) molto semplice per smussare il rumore
    xF = alfa * x + (1.0f - alfa) * xF;
    yF = alfa * y + (1.0f - alfa) * yF;
    zF = alfa * z + (1.0f - alfa) * zF;

    // Stampa "label:valore" separati da TAB > tre curve nel Plotter
    Serial.print("X:");
    Serial.print(xF, 3);
    Serial.print('\t');
    Serial.print("Y:");
    Serial.print(yF, 3);
    Serial.print('\t');
    Serial.print("Z:");
    Serial.println(zF, 3);
  } else {
    Serial.print("X:");
    Serial.print(x, 3);
    Serial.print('\t');
    Serial.print("Y:");
    Serial.print(y, 3);
    Serial.print('\t');
    Serial.print("Z:");
    Serial.println(z, 3);
  }

  delay(periodoCampionamentoMs);
}

Analizziamo nel dettaglio la parte del filtro.

xF = alfa * x + (1.0f - alfa) * xF;
yF = alfa * y + (1.0f - alfa) * yF;
zF = alfa * z + (1.0f - alfa) * zF;

xF = alfa * x + (1.0f - alfa) * xF;

  • x è il dato grezzo appena letto dall’accelerometro sull’asse X.
  • xF è il dato filtrato (uscita del filtro) per l’asse X.
  • alfa * x prende una frazione del dato nuovo (quanto “peso” dai alla misura attuale).
  • (1.0f - alfa) * xF prende una frazione del valore filtrato precedente (la “memoria”).
  • La somma dei due termini fornisce un valore ammorbidito: meno sensibile ai picchi improvvisi.

Nota sul suffisso f: indica un float letterale (utile per evitare promozioni a double in alcune piattaforme).

yF = alfa * y + (1.0f - alfa) * yF;

  • Identico ragionamento, ma applicato all’asse Y: nuova misura y + memoria del filtrato yF.
  • Mantieni alfa uguale sugli assi per coerenza visiva nel Plotter.

zF = alfa * z + (1.0f - alfa) * zF;

  • Stessa cosa per l’asse Z: la componente verticale (spesso contiene circa 1 g).
  • Filtrare Z aiuta a vedere meglio oscillazioni lente e a ridurre tremolii.

Qualche nota pratica

  1. Inizializzazione: se impostate xF = yF = zF = 0 all’inizio, i primi campioni possono “salire” gradualmente verso il valore reale (breve transitorio). Se volete partire subito “allineati”, potete inizializzare xF=yF=zF al primo dato letto.
  2. Scelta di alfa:
    0.1-0.3 > grafico molto liscio (meno rumore), risposta più lenta.
    0.4-0.7 > più reattivo, ma un po’ più “seghettato”.

Buon Coding a tutti 🙂

Progettare bene, programmare meglio: pseudocodice e diagrammi per sistemi elettronici – lezione 4/5


Ripasso di inizio anno – appunti per la classe.

Nella lezione precedente abbiamo capito che cos’è lo pseudocodice e perché ci aiuta a trasformare problemi della vita quotidiana in passi ordinati, fatti di input, decisioni e azioni. Lo abbiamo usato per pianificare una routine del mattino, un “pomodoro” di studio, uno “zaino intelligente”: esempi senza hardware, ma già con la testa da progettisti.

Oggi facciamo un passo in più: useremo lo stesso modo di pensare per progettare sistemi elettronici con Arduino. Tradurre “ciò che deve accadere” in uno pseudocodice chiaro e verificabile ci farà risparmiare tempo quando passeremo alla codifica in C/C++.

Vedremo in questa lezione come:

  • descrivere il comportamento di un sistema (sensori, attuatori, tempi, soglie) prima di scrivere codice;
  • organizzare lo pseudocodice in sezioni: costanti e pin, variabili di stato, inizializzazione, loop principale, funzioni;
  • riconoscere e usare pattern ricorrenti da laboratorio: lettura sensori, debounce, timer con millis(), macchina a stati semplici;
  • migliorare la qualità del vostro pseudocodice: nomi significativi, commenti utili, blocchi brevi, casi limite previsti;

L’obiettivo pratico è quello di fornire uno schema eseguibile del vostro sistema, uno pseudocodice che, riga per riga, si traduce in istruzioni Arduino.

Vediamo come fare.

Vi ricordo che in laboratorio non “giochiamo con led e fili”: impariamo a progettare sistemi.
Ogni sistema ha ingressi (sensori), uscite (attuatori), stati, soglie, tempi e condizioni di errore, pertanto mettere tutto in ordine prima di codificare significa:

  • sapere cosa misurare e quando;
  • chiarire quando intervenire (condizioni);
  • evitare loop confusi e delay() ovunque (inizio a dirvi che il delay è il male assoluto 🙂 );
  • testare in modo mirato (so quali valori provo e che output mi aspetto);

Quindi il risultato quale sarà?

  • Meno frustrazione;
  • meno “perché non va?”;
  • più tempo per capire.

Le fasi di lavoro saranno le seguenti:

  • progettazione: partiremo dal comportamento desiderato e lo scriveremo in pseudocodice, a blocchi.
  • traduzione: mapperemo ogni blocco in istruzioni Arduino (Serial, pinMode, digitalRead/Write, analogRead, map, millis, ecc.);
  • verifica: controlleremo che i casi limite siano previsti (pulsante che rimbalza, soglia ballerina, time-out, errori di misura);
  • codifica: solo alla fine andremo sull’IDE Arduino.

Per semplificare la fase di progettazione di seguito trovate alcune regole di base molto semplici che consiglio di trasformare in un foglio di riferimento da aggiungere all’inizio del quadernone.

Regole di base

  • scrivi una istruzione per riga;
  • usa i due punti dopo le parole chiave (SE:, PER:, MENTRE:, FUNZIONE:);
  • indenta (vai a capo e rientra) quello che sta “dentro” ad un blocco;
  • metti commenti con // per spiegare.

Esempio di forma di scrittura codice:

SE (condizione) ALLORA:
    fai questa cosa
ALTRIMENTI:
    fai quest’altra

Negli anni passati avete già programmato, ma poiché ho notato qualche disordine e dimenticanza di seguito vi elenco le strutture di base in pseudocodice che useremo come base di partenza, le scrivo io, le analizzeremo e poi userete questi blocchi come mattoncini lego per scrivere lo pseudocodice di un sistema.

Input / Output

LEGGI nome
SCRIVI "Ciao, " + nome

Variabili

imposta eta a 13
imposta temperatura a 21.5

Scelte (IF)

SE (eta >= 18) ALLORA:
    SCRIVI "Maggiorenne"
ALTRIMENTI:
    SCRIVI "Minorenne"

Ripetizioni (LOOP)
Per numero di volte:

PER i da 1 a 5:
    SCRIVI i

Finché una condizione è vera

imposta numero a 1
MENTRE (numero <= 10):
    SCRIVI numero
    numero = numero + 1

Funzioni (blocchi riutilizzabili)

FUNZIONE somma(a, b):
    RITORNA a + b

SCRIVI somma(3, 4)  // stampa 7

Liste (array) piccole

imposta voti a [7, 8, 6, 9]
per ogni v in voti:
    SCRIVI v

Nota sullo stile dei commenti

E’ importante commentare e saper commentare, ne abbiamo già parlato, ma credo sia meglio ribadirlo:

usare nomi chiari: “contaStudenti” è meglio di “c”, ricorda di usare la forma camelcase;
commenta le parti importanti:

// aggiorno il punteggio
punteggio = punteggio + 10

Usa un’idea per riga: corto e leggibile.

Errori comuni

3 errori che ritrovo sempre nelle correzioni delle esercitazioni, sia nello scrivere lo pseudocodice che il codice:

  • dimenticare di aggiornare le variabili nei cicli > il ciclo non finisce mai.
  • usare = invece di == nelle condizioni (in pseudocodice teneteli distinti);
  • non indentare: diventa confuso capire cosa sta dentro un IF o un ciclo.

Qualche esempio pratico

Pari o dispari

LEGGI n
SE (n % 2 == 0) ALLORA:
    SCRIVI "Pari"
ALTRIMENTI:
    SCRIVI "Dispari"

Massimo tra 3 numeri

LEGGI a, b, c
imposta max a a
SE (b > max) ALLORA: max = b
SE (c > max) ALLORA: max = c
SCRIVI "Massimo = " + max

Conta quanti voti ≥ 6

LEGGI N
imposta cont a 0

PER i da 1 a N:
    LEGGI voto
    SE (voto >= 6) ALLORA:
        cont = cont + 1

SCRIVI "Promossi: " + cont

Nella prossima lezione vedremo come passare dallo pseudocodice allo sketch Arduino.

Buon pseudocodice a tutti 🙂