Restituire più valori in Arduino “alla C”: output parameters con puntatori – lezione 2/2

Nella lezione precedente abbiamo visto come ottenere più risultati da una funzione usando variabili globali, riferimenti e struct.
In questa seconda lezione facciamo un passo “più vicino al C puro” e affrontiamo una tecnica molto diffusa nei linguaggi embedded: gli output parameters con puntatori.

Cosa intendiamo quando parliamo di puntatori?
Una funzione in genere può restituire un solo valore con return, ma può anche ricevere gli indirizzi di memoria delle variabili che vogliamo aggiornare, in pratica le diciamo: “scrivi i risultati direttamente qui”. Questo approccio è estremamente comune in librerie e API, è efficiente, ma richiede più attenzione: con i puntatori entrano in gioco concetti come indirizzo (&), dereferenziazione (*) e la necessità di evitare errori (ad esempio puntatori nulli).

Quando usiamo i puntatori come parametri di uscita:

  • i parametri in ingresso possono essere valori normali (int numeratore)
  • i parametri in uscita sono puntatori (int *quoziente) perché indicano l’indirizzo della variabile da modificare
  • dentro la funzione, per modificare la variabile “esterna”, dovete usare *:
    • *quoziente = ...; significa “scrivi nella variabile puntata da quoziente”

La regola mentale utile è la seguente:

  • &variabile -> “dammi l’indirizzo”
  • *puntatore -> “vai a quella variabile e usala”

Esempio 01: scambio di due valori con puntatori (swap)

nello sketch che segue viene chiarito come:

  • usare & nella chiamata
  • usare * nella funzione
  • la funzione modifica le variabili chiamanti
/*
  Prof. Maffucci Michele
  15.02.26
  Scambia due valori usando puntatori (stile C).
*/

void scambiaConPuntatori(int *a, int *b) {
  if (a == nullptr || b == nullptr) {
    return; // sicurezza: evita dereferenziazioni invalide
  }

  int temporaneo = *a;
  *a = *b;
  *b = temporaneo;
}

void setup() {
  Serial.begin(9600);
  randomSeed(analogRead(A0));
}

void loop() {
  int n1 = random(10);
  int n2 = random(10);

  Serial.print("Valori prima dello scambio (n1, n2): ");
  Serial.print(n1);
  Serial.print(", ");
  Serial.println(n2);

  scambiaConPuntatori(&n1, &n2);

  Serial.print("Valori dopo lo scambio (n1, n2):  ");
  Serial.print(n1);
  Serial.print(", ");
  Serial.println(n2);

  Serial.println();
  delay(1000);
}

La funzione non riceve copie dei numeri, ma riceve gli indirizzi di n1 e n2. Quando scrive *a = ..., sta aggiornando proprio n1 (e analogo per n2).

Esempio 02: divisione con resto (quoziente + resto) e gestione errore

Nello sketch che segue viene mostrato:

  • una funzione che produce due output;
  • un valore booleano di ritorno per indicare successo/errore;
  • controllo se viene fatta una divisione per zero;
  • validazione di puntatori (nullptr).
/*
  Prof. Maffucci Michele
  15.02.26
  Calcola quoziente e resto usando puntatori come parametri di uscita.
*/

bool dividiConRestoPtr(int numeratore, int denominatore, int *quoziente, int *resto) {
  if (denominatore == 0 || quoziente == nullptr || resto == nullptr) {
    return false;
  }

  *quoziente = numeratore / denominatore;
  *resto = numeratore % denominatore;
  return true;
}

void setup() {
  Serial.begin(9600);
  randomSeed(analogRead(A0));
}

void loop() {
  int a = random(0, 101);
  int b = random(0, 11); // include lo zero per testare l'errore

  int q = 0;
  int r = 0;

  Serial.print("Operazione: ");
  Serial.print(a);
  Serial.print(" / ");
  Serial.println(b);

  if (dividiConRestoPtr(a, b, &q, &r)) {
    Serial.print("Quoziente: ");
    Serial.println(q);

    Serial.print("Resto: ");
    Serial.println(r);
  } else {
    Serial.println("Errore: divisione per zero o parametri non validi.");
  }

  Serial.println();
  delay(1500);
}

Approfondimento

  • return comunica se l’operazione è riuscita
  • gli output veri e propri vengono scritti nei parametri *quoziente e *resto

È una struttura molto comune anche in librerie C, dove spesso vedete funzioni del tipo:

  • bool readSensor(int *value);
  • int parseData(..., char *outBuffer); (qui l’output è un buffer)

Errori tipici con i puntatori e come evitarli

Errore 01: dimenticare & nella chiamata

scambiaConPuntatori(n1, n2);

state passando interi dove la funzione si aspetta indirizzi -> errore di compilazione (oppure warning serio).

Versione corretta:

scambiaConPuntatori(&n1, &n2);

Errore 02: dereferenziare un puntatore nullo

Dentro una funzione, *p = ... è pericoloso se p è nullptr

per questo motivo negli esempi trovate sempre:

if (p == nullptr) return false;

Errore 03: confondere * e &

Come indicato ad inizio post:

  • &si usa quando chiamate per dare l’indirizzo
  • * si usa dentro la funzione per leggere/scrivere la variabile puntata

Regola generale: quando scegliere i puntatori e quando no

Mi ripeterò però ritengo utile scriverlo.

I puntatori servono quando volete dire a una funzione:

“Non ti sto passando un valore. Ti sto passando dove si trova quel valore in memoria, così puoi modificarlo direttamente.”

dovete fare attenzione perché state lavorando con indirizzi di memoria.

Quando usare i puntatori

01. Quando lavorate in “stile C” o con librerie in C

Molte librerie e molti esempi “embedded” storici sono scritti così:

    • la funzione ritorna true/false (o un codice) per dire se tutto ok
    • i risultati veri e propri vengono scritti nei parametri puntatore

Esempio:

bool leggiSensore(int *valore);

02. Quando dovete far riempire un “contenitore” (buffer/array)

Se dovete far scrivere a una funzione dentro un array o una stringa, spesso userete puntatori.

Esempio:

    • leggere dati e salvarli in un array di byte
    • costruire una stringa di output

In quel caso, “passare l’indirizzo” è naturale perché l’array è già, di fatto, un’area di memoria.

Quando NON usare i puntatori

01. Quando potete usare i riferimenti (&) in Arduino (C++)

Se state scrivendo codice Arduino “normale”, i riferimenti fanno lo stesso lavoro dei puntatori (modificano variabili esterne), ma sono più leggibili e meno soggetti a errori.

Con riferimento:

void scambia(int &a, int &b);

Con puntatori:

void scambia(int *a, int *b);

Per chi inizia con la programmazione, il riferimento comunica meglio:

“questa variabile può essere modificata dalla funzione”

02. Quando il vostro obiettivo è imparare le l’uso delle funzioni senza complicarvi la vita

I puntatori aggiungono concetti nuovi:

    • & (indirizzo)
    • * (dereferenziazione)
    • rischio di puntatori nulli / sbagliati

Se state iniziando con la programmazione e volete capire “come passare dati alle funzioni”, come spiegato nella lezione precedente, è spesso meglio partire da:

    • per valore (copia)
    • per riferimento (modifica)
    • struct (pacchetto di dati)

Buon Coding a tutti 🙂

randomSeed() su Arduino come usarla

In riferimento al precedente post: “Come ottenere più valori da una funzione in Arduino: variabili globali, riferimenti e struct – lezione 1/2” un utente mi ha scritto in privato per chiedermi chiarimenti in merito all’uso di randomSeed(), nello specifico di randomSeed(analogRead(A0)). La spiegazione che ho fornito è diventata alla fine qualcosa che posso riutilizzare con i miei studenti quindi ne ho realizzato un post sul sito in modo che possa essere letto da tutti.

Quando in uno sketch usate random(), state usando un generatore pseudo-casuale: i numeri “sembrano” casuali, ma in realtà seguono una sequenza deterministica. Se riavviate la scheda e non fate nulla, spesso la sequenza riparte uguale. Per evitare questo comportamento (molto evidente nei giochi, nelle simulazioni e nelle attività di laboratorio), Arduino mette a disposizione randomSeed(), che inizializza il punto di partenza della sequenza.

Sintetizzo cosa fa nel dettaglio randomSeed()

  • random() genera numeri pseudo-casuali.
  • randomSeed(seed) imposta il seme (seed), cioè il valore da cui parte la sequenza.

questo vuol dire che:

  • Seed uguale > sequenza uguale.
  • Seed diverso > sequenza diversa.

L’effetto di randomSeed(analogRead(A0)) utilizzato nella lezione precedente è duplice:

  1. legge un valore analogico da A0 con analogRead(A0)
  2. usa quel valore come seme per randomSeed(...)

L’idea (consigliata anche nella documentazione Arduino) è: se A0 è scollegato, la lettura è influenzata dal “rumore” elettrico ambientale e tende a variare; quindi anche il seed cambia e, a ogni reset, random() produce una sequenza diversa.

Però attenzione, un pin “floating” non garantisce casualità perfetta; spesso fornisce poca entropia, ma per scopi didattici (giochi, simulazioni, esercizi) è in genere sufficiente. Ovviamente non è una tecnica adatta a sicurezza/criptografia.

Precisazione: differenze tra UNO R3 e UNO R4

  • Arduino UNO R3: analogRead() è a 10 bit: valori 0–1023.
  • Arduino UNO R4: per compatibilità, analogRead() parte di default a 10 bit (0–1023), ma l’UNO R4 supporta risoluzioni più alte (fino a 14 bit) tramite analogReadResolution().

Questa informazioni può essere utile anche per il seed: più risoluzione vuol dire più livelli possibili nella lettura analogica, quindi (potenzialmente) seed più variabile.

Di seguito alcune proposte di attività da svolgere a scuola o per esercitarsi.
Tutti gli esempi stampano su Serial Monitor. Usate 9600 baud e, quando richiesto, lasciate A0 scollegato.

Attività 01: serie che si ripete – (senza randomSeed)

Obiettivo: vedere che, senza inizializzazione, la sequenza tende a ripetersi dopo un reset
Consegna: fate partire lo sketch, annotate i primi 10 numeri, premete reset e confrontate.

/*
  Prof. Maffucci Michele
  14.02.26
  Attività 01: serie che si ripete - (senza randomSeed)
*/

void setup() {
  Serial.begin(9600);
}

void loop() {
  Serial.println("Primi 10 numeri pseudo-casuali (senza seed):");
  for (int i = 0; i < 10; i++) {
    Serial.println(random(0, 100));
  }
  Serial.println("--- Premi reset della scheda e confronta ---\n");
  delay(3000);
}

Attività 02: Seed fisso = sequenza ripetibile (utile nei test)

Obiettivo: capire che un seed costante produce sempre la stessa sequenza (utile per il debug).
Consegna: verificare che a ogni reset i numeri restano identici.

/*
  Prof. Maffucci Michele
  14.02.26
  Attività 02: capire che un seed costante produce sempre la stessa sequenza (utile per il debug)
*/

void setup() {
  Serial.begin(9600);
  randomSeed(12345); // seed fisso
}

void loop() {
  Serial.println("Primi 10 numeri con seed fisso (12345):");
  for (int i = 0; i < 10; i++) {
    Serial.println(random(0, 100));
  }
  Serial.println("--- Premi reset: deve ripetersi uguale ---\n");
  delay(3000);
}

Attività 03: Seed “rumoroso” con analogRead(A0) (A0 scollegato)

Obiettivo: ottenere sequenze diverse a ogni reset usando randomSeed(analogRead(A0)).
Consegna:

  1. eseguite con A0 scollegato > osservate seed e sequenza
  2. collega A0 a GND o 5V > osservate come seed e sequenza diventano più “stabili” (meno variabili)
/*
  Prof. Maffucci Michele
  14.02.26
  Attività 03: ottenere sequenze diverse a ogni reset usando randomSeed(analogRead(A0))
*/

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

  // A0 deve essere scollegato per sfruttare la variabilità del rumore
  int seme = analogRead(A0);
  randomSeed(seme);

  Serial.print("Seed letto da A0: ");
  Serial.println(seme);
  Serial.println();
}

void loop() {
  Serial.println("Primi 10 numeri con seed da A0:");
  for (int i = 0; i < 10; i++) {
    Serial.println(random(0, 100));
  }
  Serial.println("--- Premi reset: dovrebbero cambiare ---\n");
  delay(3000);
}

Attività 04: utilizzo di UNO R4 per avere seed più “ricco” aumentando la risoluzione (12/14 bit)

Obiettivo: su UNO R4 aumentare la risoluzione ADC e “mescolare” più letture per un seed più variabile.
Consegna:

  • fate 5 reset su UNO R3 e 5 reset su UNO R4, annotate i seed e valutate quanto cambiano
  • su UNO R4 provate anche analogReadResolution(12) e confronta con 14

Lo sketch che segue viene compilato anche su UNO R3: la parte analogReadResolution() viene attivata solo su UNO R4.

/*
  Prof. Maffucci Michele
  14.02.26
  Attività 04: utilizzo di UNO R4 per avere seed più "ricco" aumentando la risoluzione (12/14 bit)
*/

unsigned long creaSeedDaRumoreA0() {
  unsigned long seed = 0;

  // Su UNO R4 puoi aumentare la risoluzione (10 bit default, fino a 14 bit)
#if defined(ARDUINO_UNOR4_MINIMA) || defined(ARDUINO_UNOR4_WIFI)
  analogReadResolution(14);
#endif

  // Mescolo 32 letture per raccogliere più variabilità
  for (int i = 0; i < 32; i++) {
    seed = (seed << 1) ^ (unsigned long)analogRead(A0);
    delay(2);
  }
  return seed;
}

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

  Serial.println("Seed avanzato da A0 (A0 scollegato):");
  unsigned long seed = creaSeedDaRumoreA0();
  Serial.print("Seed calcolato: ");
  Serial.println(seed);

  randomSeed(seed);
  Serial.println();
}

void loop() {
  Serial.println("Primi 10 numeri con seed avanzato:");
  for (int i = 0; i < 10; i++) {
    Serial.println(random(0, 100));
  }
  Serial.println("--- Reset e confronta ---\n");
  delay(3000);
}

Ora vi propongo un piccolo progetto, il classico dado, che sicuramente riuscirete a svolgere velocemente.

Esercizio: “dado elettronico”

Obiettivo: usare random() in un contesto concreto e verificare che con randomSeed i lanci non siano sempre uguali dopo reset.

Vi lascio qualche giorno per provare a sviluppare una soluzione e poi ve ne propongo una io. 🙂

Terminiamo questa lezione riassumendo quanto visto:

  • random() = pseudo-casuale (sequenza deterministica);
  • randomSeed() sposta il punto di partenza della sequenza;
  • randomSeed(analogRead(A0)) funziona solo se A0 varia davvero (meglio scollegato o con una sorgente “rumorosa”);
  • UNO R4 può aumentare la risoluzione ADC con analogReadResolution() (default 10 bit per compatibilità).

Buon Coding a tutti 🙂

Come ottenere più valori da una funzione in Arduino: variabili globali, riferimenti e struct – lezione 1/2

Durante la correzione delle esercitazioni di laboratorio mi accorgo spesso che il passaggio dei valori alle funzioni è un punto che genera parecchia confusione. Per questo ho deciso di preparare una breve lezione di ripasso, per chiarire i concetti fondamentali e aiutare a scegliere l’approccio più corretto nello sviluppo degli sketch.

Uno dei problemi molto comuni quando si programma in C/C++ su Arduino è: come ottenere più di un valore da una funzione?

In molti casi vorremmo che una funzione ci restituisse, ad esempio, due risultati (come nello scambio di due numeri), ma il linguaggio prevede che una funzione possa avere un solo valore di ritorno con l’istruzione return. Questo non significa che siamo bloccati: esistono più strategie corrette per gestire più risultati in modo chiaro e sicuro.

Partiremo da una soluzione semplice e intuitiva, basata su variabili globali, utile per capire il concetto ma generalmente sconsigliata nei programmi reali perché può creare effetti collaterali difficili da controllare. Passeremo poi a un approccio molto più robusto: il passaggio per riferimento, che permette a una funzione di modificare direttamente le variabili del chiamante senza ricorrere a globali. Infine vedremo l’uso dell’istruzione struct, cioè “contenitori” di più campi, che consentono di raggruppare dati correlati e di restituirli come un unico oggetto.

L’obiettivo non è solo far funzionare gli esempi, ma capire cosa succede davvero in memoria quando una funzione riceve parametri “per valore” (copie) oppure “per riferimento” (accesso diretto all’originale) e perché la scelta del metodo influisce su leggibilità, manutenzione e affidabilità del codice. Al termine della lezione saprete riconoscere quale tecnica usare a seconda del problema e scrivere funzioni più pulite, modulari e facili da riutilizzare.

Vedremo poi nella prossima lezione una quarta variante molto diffusa nel mondo embedded.

01. Versione “semplice” (ma sconsigliata): usare variabili globali

Esempio 01: scambio di valori

La funzione non “restituisce” due valori: modifica direttamente due variabili definite fuori da loop().

/*
  Prof. Maffucci Michele
  13.02.26
  Scambio di due valori usando variabili globali.
*/

int valoreA;
int valoreB;

void scambiaGlobali() {
  int temporaneo = valoreA;
  valoreA = valoreB;
  valoreB = temporaneo;
}

void setup() {
  Serial.begin(9600);
}

void loop() {
  valoreA = random(10);
  valoreB = random(10);

  Serial.print("Valori prima dello scambio (A, B): ");
  Serial.print(valoreA);
  Serial.print(", ");
  Serial.println(valoreB);

  scambiaGlobali();

  Serial.print("Valori dopo lo scambio (A, B):  ");
  Serial.print(valoreA);
  Serial.print(", ");
  Serial.println(valoreB);

  Serial.println();
  delay(1000);
}

Esempio 02: la funzione restituisce minimo e massimo aggiornando variabili globali.

Sulla serial monitor vengono mostrati ogni 5 secondi: la serie di 20 numeri generati, il minimo della serie, il massimo della serie.

/*
  Prof. Maffucci Michele
  13.02.26
  Calcola minimo e massimo su una serie di campioni usando variabili globali.
*/

int minimoCampione;
int massimoCampione;

void calcolaMinMaxGlobali(int numeroCampioni) {
  minimoCampione = 9999;
  massimoCampione = -9999;

  Serial.print("Campioni: ");

  for (int i = 0; i < numeroCampioni; i++) {
    int campione = random(0, 101); // 0..100

    // Stampa i campioni separati da virgola
    Serial.print(campione);
    if (i < numeroCampioni - 1) {
      Serial.print(", ");
    } else {
      Serial.println(); // a capo dopo l'ultimo campione
    }

    // Aggiorna minimo e massimo
    if (campione < minimoCampione) {
      minimoCampione = campione;
    }
    if (campione > massimoCampione) {
      massimoCampione = campione;
    }
  }
}

void setup() {
  Serial.begin(9600);
  randomSeed(analogRead(A0));
}

void loop() {
  const int numeroCampioni = 20;

  Serial.print("Genero ");
  Serial.print(numeroCampioni);
  Serial.println(" campioni casuali (0..100) e calcolo min/max...");

  calcolaMinMaxGlobali(numeroCampioni);

  Serial.print("Minimo trovato: ");
  Serial.println(minimoCampione);

  Serial.print("Massimo trovato: ");
  Serial.println(massimoCampione);

  Serial.println();
  delay(5000);
}

02. Passaggio per riferimento

Esempio 01: Scambia due valori passati per riferimento.

/*
  Prof. Maffucci Michele
  13.02.25
  Scambia due valori passati per riferimento.
*/

void scambiaPerRiferimento(int &a, int &b) {
  int temporaneo = a;
  a = b;
  b = temporaneo;
}

void setup() {
  Serial.begin(9600);
}

void loop() {
  int numero1 = random(10);
  int numero2 = random(10);

  Serial.print("Valori prima dello scambio (n1, n2): ");
  Serial.print(numero1);
  Serial.print(", ");
  Serial.println(numero2);

  scambiaPerRiferimento(numero1, numero2);

  Serial.print("Valori dopo lo scambio (n1, n2):  ");
  Serial.print(numero1);
  Serial.print(", ");
  Serial.println(numero2);

  Serial.println();
  delay(1000);
}

Esempio 02: divisione con resto (quoziente + resto)

Nello sketch che segue una funzione calcola due risultati (quoziente e resto) tramite riferimenti, e in più restituisce true/false per gestire il caso del divisore zero.

/*
  Prof. Maffucci Michele
  13.02.26
  Calcola due risultati (quoziente e resto) tramite riferimenti, 
  restituisce true/false per gestire il caso del divisore zero.
*/

bool dividiConResto(int numeratore, int denominatore, int &quoziente, int &resto) {
  if (denominatore == 0) {
    return false; // errore: divisione per zero
  }

  quoziente = numeratore / denominatore;
  resto = numeratore % denominatore;
  return true;
}

void setup() {
  Serial.begin(9600);
  randomSeed(analogRead(A0));
}

void loop() {
  int a = random(0, 101);   // 0..100
  int b = random(0, 11);    // 0..10 (include lo zero apposta)

  int q = 0;
  int r = 0;

  Serial.print("Operazione: ");
  Serial.print(a);
  Serial.print(" / ");
  Serial.println(b);

  if (dividiConResto(a, b, q, r)) {
    Serial.print("Quoziente: ");
    Serial.println(q);

    Serial.print("Resto: ");
    Serial.println(r);
  } else {
    Serial.println("Errore: divisione per zero (b = 0).");
  }

  Serial.println();
  delay(1500);
}

03. Scambio di valori versione migliorata. Trattare più valori insieme: usare una struct

Con la struct impacchettiamo più campi in un solo tipo (una “coppia”), la passiamo alla funzione e la funzione restituisce una nuova struct con i campi scambiati.

Esercizio 01: scambio di valori

/*
  Prof. Maffucci Michele
  13.02.26
  Scambio di valori usando una struct.
*/

struct Coppia {
  int primo;
  int secondo;
};

Coppia scambiaCoppia(Coppia c) {
  int temporaneo = c.primo;
  c.primo = c.secondo;
  c.secondo = temporaneo;
  return c;
}

void setup() {
  Serial.begin(9600);
}

void loop() {
  Coppia dati = { random(10), random(10) };

  Serial.print("Valori prima dello scambio (p, s): ");
  Serial.print(dati.primo);
  Serial.print(", ");
  Serial.println(dati.secondo);

  dati = scambiaCoppia(dati);

  Serial.print("Valori dopo lo scambio (p, s):  ");
  Serial.print(dati.primo);
  Serial.print(", ");
  Serial.println(dati.secondo);

  Serial.println();
  delay(1000);
}

Esempio 02: Lettura analogica completa (raw + volt + percentuale)

La funzione ritorna un oggetto con tre valori: lettura grezza, tensione stimata e percentuale.

/*
  Prof. MAffucci Michele
  13.02.26
  Restituisce più valori raggruppandoli in una struct.
*/

struct LetturaAnalogica {
  int raw;          // 0..1023
  float volt;       // tensione stimata
  int percentuale;  // 0..100
};

const int PIN_INGRESSO = A0;
const float VREF = 5.0; // se usi una scheda a 3.3V, imposta 3.3

LetturaAnalogica leggiAnalogicoCompleto(int pin) {
  LetturaAnalogica risultato;

  risultato.raw = analogRead(pin);
  risultato.volt = risultato.raw * (VREF / 1023.0);
  risultato.percentuale = map(risultato.raw, 0, 1023, 0, 100);

  return risultato;
}

void setup() {
  Serial.begin(9600);
}

void loop() {
  LetturaAnalogica misura = leggiAnalogicoCompleto(PIN_INGRESSO);

  Serial.print("Ingresso A0 -> raw: ");
  Serial.print(misura.raw);

  Serial.print(" | V: ");
  Serial.print(misura.volt, 2);

  Serial.print(" | %: ");
  Serial.println(misura.percentuale);

  delay(500);
}

Abbiamo visto tre strategie diverse per gestire più risultati in una funzione. E’ fondamentale scegliere quale usare nei vostri sketch, è importante capire bene cosa cambia tra passaggio per valore, passaggio per riferimento e uso di una struct.

Riassumo di seguito.

Passaggio dei valori a una funzione

Quando viene chiamata una funzione, potete consegnare i dati in due modi principali:

  1. Per valore (default)
  • la funzione riceve una copia dei parametri;
  • se dentro la funzione modificate quei parametri, state modificando la copia, non le variabili originali;
  • risultato: fuori dalla funzione i valori non cambiano.
  1. Per riferimento (&) — tipico in Arduino perché è C++

  • la funzione riceve un alias delle variabili originali;
  • dentro la funzione, quando assegnate a = …, state scrivendo proprio nella variabile chiamante.
  • risultato: fuori dalla funzione i valori cambiano davvero.

Nota: in C “puro” non esistono i riferimenti, quindi si userebbero puntatori (int *a, int *b). Arduino, però, compila in C++: i riferimenti sono disponibili.

Quando usare una struct (o un tipo “aggregato”)

La struct è utile quando:

  • volete trattare più valori come un unico oggetto logico (es. coordinate, min/max, stato di sensori, ecc.);
  • volete restituire più informazioni in un colpo solo (una sola return, ma con più campi dentro);
  • volete migliorare leggibilità: c.primo, c.secondo comunica il significato meglio di due variabili scollegate.

Costo/beneficio nell’uso della struct

  • Pro: codice più espressivo, dati “impacchettati”, interfaccia pulita.
  • Contro: c’è una copia della struct quando la passi/ritorni (di solito trascurabile se la struct è piccola). Su microcontrollori molto limitati, per struct grandi conviene passare per riferimento anche la struct.

Come detto ad inizio di questo post, nella prossima lezione vedremo una quarta variante molto diffusa nel mondo embedded: gli output parameters con puntatori (stile C) e una versione più moderna che usa const per rendere immediatamente chiaro quali dati sono solo in ingresso e quali vengono modificati dalla funzione.

Buon Coding a tutti 🙂

Arduino nello zaino: 4 “laboratori” trasportabili stampabili in 3D per avere sempre una base di prototipazione pronta

Chi fa didattica laboratoriale (o semplicemente ama sperimentare) lo sa: il problema non è avere gli strumenti, ma riuscire a portare con sé una dotazione minima che permetta di accendere il cervello “maker” ovunque ci si trovi.

Io ho già i miei laboratori completi, a casa e a scuola, dove posso fare tutto ciò che mi serve, però nello zaino porto sempre una piccola base di sperimentazione: una scheda Arduino, una breadboard, qualche cavetto e pochi componenti selezionati. Non è un “mini laboratorio” nel senso classico del termine, ma è qualcosa di altrettanto prezioso: un sistema ordinato e trasportabile che riduce l’attrito e rende immediata qualsiasi micro-attività.

In questo post vi segnalo 4 tipologie di contenitori/workstation stampabili in 3D pensate proprio per questo: tenere insieme scheda, breadboard, jumper e componenti, proteggere il setup e arrivare in aula (o ovunque) già pronto a partire.

Perché ritengo che questi organizer siano utili anche se avete un laboratorio completo

  • setup più veloce: appoggi, apri, colleghi l’USB e inizi;
  • ordine e componenti “a prova di zaino”: meno dispersione, meno “dov’è finito quel cavetto?”;
  • micro-attività replicabili: perfetti per dimostrazioni, tutoraggi, ASL, attività itineranti, laboratori in aule non attrezzate;
  • continuità nelle sperimentazioni personali: potete riprendere un prototipo esattamente dov’era rimasto, senza ricostruire tutto da zero.

All’organizer associo anche un piccolo contenitore rigido in cui dispongo ulteriori componenti elettronici.

The Folding Arduino Lab (il “classico” da zaino)

È quello che uso da sempre: The Folding Arduino Lab di Jason Welsh già segnalato qualche tempo fa su questo sito.
L’idea è semplice: un contenitore compatto che integra Arduino + breadboard (400 punti) + cassetti per cavetti e componenti. In uno spazio ridotto (indicativamente 10×9×8 cm) vi portate dietro l’essenziale.

I motivi per cui uso questa soluzione sono:

  • struttura “a libro”: protegge il setup e lo rende trasportabile, ovviamente non bisogna pretendere molti componenti sulla breadboard;
  • due vani/cassetti: ottimi per jumper, LED, resistenze, pulsanti, piccoli sensori;
  • è un progetto con molte varianti e remix: facile trovare adattamenti e accessori;
  • nota personale: l’autore ha sviluppato diverse versioni; io attualmente utilizzo la versione 2, su cui ho montato un Arduino UNO R4 WiFi.

Vi segnalo che con questo kit potete evitare, per le cerniere, l’uso di elementi metallici che possono essere sostituiti con filamento per la stampa 3D che risultano relativamente robusti e rimovibili.

Supporto/Stazione di lavoro modulare per Arduino UNO e breadboard (modulare e “scalabile”)

Questa soluzione è pensata come una workstation regolabile e modulare, ideale se vuoi una base ordinata non solo per trasporto, ma anche per lavorare “pulito” su banco (e poi richiudere e portare via).
La Modular Arduino UNO Breadboard Holder/Workstation (V2) è dichiarata come progetto modulare, con inserti, tray e mount selezionabili, e con impostazioni/accortezze legate alla stampa “print-in-place” in alcune parti.

Caratteristiche interessanti

  • modularità reale: potete scegliere vassoi/inserti in base a cosa usate (jumper, sensori, viteria, ecc.);
  • approccio “workstation” più che “scatola”: molto comoda per lavorare in modo ordinato;
  • ottima se volete aggiungere nel tempo moduli e supporti (display, sensori, ecc.).

Portable Arduino Lab (PAL) – molto compatto

Il Portable Arduino Lab (PAL) è una versione più piccola della workstation, pensata esplicitamente per “piccoli progetti on the go”, è dichiarato compatibile con Arduino UNO R3 e UNO R4.

Cosa mi piace

  • È progettato come “compagno di laboratorio”: base + chiusura + possibilità di storage;
  • ha indicazioni di stampa molto chiare: per esempio layer height 0,2 mm e attenzione alle tolleranze della cerniera/parti in movimento;
  • pPrevede anche opzioni e aggiornamenti: ad esempio supporti per breakout (ESP32 / Arduino Nano) sono citati tra gli update del progetto.

Nota pratica

In alcune configurazioni richiede viteria (M3x25) e una piccola fase di assemblaggio; non è un difetto, ma una scelta per robustezza e funzionalità.

Arduino R4 Laboratorio Portatile – “print in place” dedicato a UNO R4

Questa soluzione è centrata su Arduino UNO R4 e punta alla massima portabilità con la logica “print-in-place”. È indicato l’uso con breadboard da 400 punti e il fissaggio della scheda con viti M2.5 (fino a una certa lunghezza). (Seguire il link per prelevare i file per la stampa)

Scegliere questa soluzione se:

  • lavorate principalmente con UNO R4 e volete un contenitore dedicato.
  • se vi interessa un progetto già impostato con un layout essenziale: scheda + breadboard + vano.

Cosa mettere nello zaino

Se volete rendere davvero efficace uno di questi organizer, la differenza la fa la scelta dei componenti. Io ragiono per “massima resa didattica con minimo volume”:

  • Arduino (nel mio caso UNO R4 WiFi) + cavo USB;
  • breadboard 400 punti + cavetti jumper (M-M, M-F, F-F in piccola quantità);
  • set micro: LED + resistenze, 1–2 pulsanti, 1 potenziometro;
  • 1 sensore “jolly” (LDR o temperatura/umidità) + 1 attuatore (buzzer o micro-servo);
  • mini cacciavite / pinzetta, un paio di cavetti Dupont extra;
  • (opzionale) Power bank se volete lavorare senza PC per attività specifiche.

Sto realizzando in questi mesi una versione di un mini laboratorio trasportabile contenuto in una rugged bag, le classiche valigette a tenuta stagna molto robuste, le uso spesso per la realizzazione di giochi escape o per contenere apparati delicati, se riuscirò vi mostrerò più avanti il risultato.

Buon Making a tutti 🙂

Domande dagli utenti: “puoi chiarire il concetto di negativo in comune?”

Continuo con la serie delle domande che mi sono giunte sul gruppo Facebook BBC micro:bit Italy, questa volta dall’amico: Davide

ciao Michele Maffucci, uno degli altri “problemi” che ho notato che non capiscono, quando alimenti separatamente, il “negativo in comune”
Magari un giorno affronta l’argomento 🙂

Quando si alimenta separatamente (micro:bit o Arduino da una fonte e motori/driver da un’altra), molti si bloccano sul concetto di “negativo in comune”. Un modo semplice per capirlo è l’esempio dell’ascensore che spesso faccio durante le mie lezioni.

Immagina due persone che vogliono incontrarsi in un edificio:

  • la prima usa come riferimento il piano terra e dice: “sono al 2° piano”;
  • la seconda, però, considera “piano terra” il 1° piano (ha spostato lo zero): anche lei dice “sono al 2° piano”, ma in realtà si trova su un livello diverso.

Risultato: usano numeri uguali, ma riferiti a zeri diversi, quindi non riescono a coordinarsi e incontrarsi.

In elettronica succede la stessa cosa: un segnale “alto” (ad esempio 3,3 V) significa 3,3 V rispetto a un riferimento, cioè rispetto alla massa (GND). Se micro:bit e driver/servo non condividono la stessa massa, quel “3,3 V” può non essere interpretato correttamente perché lo “zero” dell’uno non coincide con lo “zero” dell’altro.

Nota importante: massa e terra non sono la stessa cosa (anche se a volte coincidono)

  • Massa / GND (0 V di riferimento): è il potenziale comune di riferimento del circuito, spesso chiamato anche 0 V. È il “piano terra” della nostra analogia: lo zero rispetto a cui misuriamo e capiamo i segnali.
  • Terra / PE (Protective Earth – protezione): è il collegamento all’impianto di terra, usato soprattutto per sicurezza elettrica (scariche, guasti, schermature).

In molti dispositivi alimentati da rete, la massa del circuito può essere collegata alla terra, e in quel caso massa e terra finiscono per trovarsi allo stesso potenziale e spesso si parla di “0 V”.

In sistemi a batteria (micro:bit, driver, servo, pacchi AA/AAA) come ad esempio i nostri robot didattici, non esiste una “terra” fisica collegata all’impianto: esiste solo la massa come riferimento comune.

Quindi, quando diciamo “mettere il negativo in comune”, intendiamo:

“Rendere comune il riferimento (GND) tra i dispositivi che devono scambiarsi segnali.”

in altro modo:

GND micro:bit < - > GND driver/servo < - > GND batteria motori

In breve: potete tenere alimentazioni separate, ma dovete rendere comune il riferimento (GND), altrimenti i segnali non hanno un “piano zero” condiviso e il controllo diventa instabile.

Schema pratico

GND micro:bit ─────────┐
GND driver/motor board ├──> punto massa comune
GND batteria motori ───┘

Il concetto è: micro:bit invia il segnale, il driver lo interpreta, ma entrambi devono riferirsi allo stesso “0 V”.

Schema di collegamento – casi tipici

A. micro:bit + driver motori DC (ponte H / Motor driver)

  • Batteria motori + (positivo) > Vmot / +VIN driver
  • Batteria motori (negativo) > GND driver
  • micro:bit pin (PWM/direzione) > IN1/IN2/ENA… (driver)
  • micro:bit GND > GND driver (fondamentale)

IMPORTANTE: non serve collegare i “positivi” tra loro; serve collegare i GND.

B. micro:bit + servo (alimentazione servo separata)

  • Batteria servo + (positivo) > V+ servo
  • Batteria servo (negativo) > GND servo
  • micro:bit pin segnale > SIG servo
  • micro:bit GND > GND servo

Senza GND in comune il servo può tremare, non rispondere o muoversi in modo erratico.

C. micro:bit alimentata e motori sullo stesso pacco batterie (solo se il pacco è adeguato)

  • Stesso pacco batterie > micro:bit (tramite regolazione corretta) + driver;
  • GND è già comune per costruzione;
  • è pratico, ma attenzione ai disturbi: spesso conviene comunque separare la potenza motori.

Accorgimenti costruttivi utili

  • collegare “a stella” le masse, ovvero un punto comune su cui colleghiamo tutte le masse vicino al driver/alimentazione motori, evitare se possibile collegamenti di masse in cascata;
  • mantenere i cavi motore e quelli di segnale separati quando possibile;
  • aggiungere condensatori di disaccoppiamento vicino al driver/servi (se il kit non li integra già).

Per approfondimenti consiglio la lettura del post: Sensori e attuatori lontani da Arduino: guida pratica al cablaggio corretto i consigli che vengono forniti possono essere adottati anche per la costruzione dei nostri robot didattici.

Questi concetti verranno approfonditi in modo pratico durante il mio prossimo corso di robotica.

Buon Making a tutti.