
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:
- Arduino – istruzione “do…while”: eseguire almeno una volta, poi verificare
- Arduino – istruzione “do…while” – soluzione esercizi proposti
- Arduino – while: ripetere finché la condizione è vera (controllo in ingresso)
- Arduino – istruzione while – soluzione esercizi proposti
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
0aN-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 verificai <N-1prima di scrivere e poi chiudere conbuf[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', valutateStringdi 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:
- Accumulo byte finché ce n’è (available()>0).
- Transizioni di stato:
IN_ATTESA> ignora spazi > primo carattere utile >IN_TOKENIN_TOKEN> caratteri “nome comando”; se arriva':'>IN_VALOREIN_VALORE> accumulo fino al newline
- Newline
\n= “fine comando”: chiudo le stringhe con'\0'e chiamoesegui(...) - protezione buffer: sempre
DIM-1per lasciare spazio al'\0'. Char extra > scartati - niente blocchi:
nessun readString/parse*; laloop()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:
- “lamentosi”
- studenti e non, bravi o svogliati
- a chi si dimentica che ciò che scrivo è gratuito ed in primis è rivolto ai miei studenti che hanno la priorità
- 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:
- è stato abbastanza faticoso scrivere questa lezione, pertanto non troverete le consuete dimostrazioni animate alla fine di ogni sketch
- 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
- 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
- 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 🙂
















Nel primo esercizio della lezione: