Arduino nello zaino, upgrade: un saldatore TS101 in un rugged case stampato in 3D

 

Nel post “Arduino nello zaino” raccontavo l’idea di fondo: non portarsi dietro un mini-laboratorio completo, ma una dotazione minima, ordinata e pronta per qualsiasi micro-attività (in aula, in laboratorio, in giro).

Oggi aggiungo un tassello importante, l’uso di un saldatore elettrico portatile per la realizzazione di circuiti elettronici.

Ne ho provati tantissimi, a gas, a batteria, ma da qualche tempo uso il Miniware TS101, perché unisce portabilità, alimentazione flessibile e controllo della temperatura.

Con Arduino, ma in generale nella realizzazione di circuiti elettronici, prima o poi capita sempre almeno uno di questi scenari:

  • un cavetto Dupont che si sfila/si rompe e volete rifare un collegamento pulito;
  • un sensore o un connettore che volete rendere più robusto (saldatura + guaina termorestringente);
  • una piccola riparazione al volo (header, pin storti, fili su jack o morsetti);
  • saldare su circuiti PCB o millefori.

inoltre l’uso di un saldatore di queste dimensioni resta coerente con la logica che descrivevo nel post precedente: setup rapido, ordine, micro-attività replicabili.

Caratteristiche del TS101

Alimentazione: USB-C PD e DC “classico”

Il TS101 supporta due ingressi di alimentazione:

  • DC5525 (9–24 V) da alimentatore o batteria
  • USB-C Power Delivery (PD) da 9 V in su (caricatore PD / power bank PD, ecc.)

IMPORTANTE: non vanno usate contemporaneamente le due alimentazioni.

Nel manuale utente trovate anche una tabella che collega tensione/potenza e tempo minimo per passare da 30°C a 300°C (valori dichiarati):

  • 9V (≈9W): ~95 s
  • 12V (≈16W): ~43 s
  • 16V (≈30W): ~22 s
  • 19V (≈40W): ~15 s
  • 24V (≈65W): ~9 s

Potenza e profili PD

  • In DC lavora tipicamente 9–24 V fino a 65 W max.
  • In USB-C PD può arrivare (a seconda di firmware e alimentatore) fino a 90 W max con PD 3.1.

Range temperatura e stabilità

  • 50–400 °C con stabilità dichiarata ±2%.

Display

  • Display OLED più grande (128×32) rispetto a TS100, menu più ricco, preset e opzioni.

Comandi e uso base

  • Pulsante A: avvio riscaldamento / regolazione
  • Pulsante B: impostazioni / regolazione
  • OLED con icone di stato (boost, movimento, sleep, ecc.)
  • Presenza di vite di terra (ground screw)

Preset e regolazione temperatura

Potete lavorare con temperature preimpostate T1/T2/T3, oppure regolare “al volo”.

Boost mode

In riscaldamento, tenendo premuto A entri in boost mode: la punta sale alla temperatura “Boost” finché tenete premuto; rilasciando, torna alla temperatura di lavoro.

Sleep/Standby

  • se in working mode il TS101 resta fermo per 180 s (default), entra in sleep (compare “zZ”) e la punta scende alla “Sleep Temp”;
  • quando viene rilevato movimento, esce dallo sleep e torna in working mode;
  • se resta fermo in sleep per 240 s (default), passa in standby; dopo ulteriore tempo, lo schermo si spegne;
  • la lettera “M” sul display indica che il TS101 si sta muovendo;
  • parametro MsenUnit (sensibilità 1–5: più alto = più sensibile).

Configurazione rapida via file

Una funzione molto interessante è la gestione tramite file:

    1. collegate il TS101 al PC con cavo dati USB-C
    2. compare un disco virtuale
    3. modificate CONFIG.TXT e i parametri vengono aggiornati

Questo è ottimo per preparare un “config” standard (temperature preset, tempi sleep, luminosità, sensibilità movimento) identica per più dispositivi.

Firmware update

  • tenete premuto A;
  • collegate via USB-C al PC (entra in DFU mode);
  • copiate il file firmware nel disco virtuale.

Sicurezza e limiti termici

  • range punta: 50°C–400°C;
  • dopo 5 minuti ad alta potenza sopra 350°C (o uso prolungato) il controller può arrivare a ~50°C;
  • quando non in uso, spegnere per evitare rischi;
  • se compare “No tip!”, la punta non è inserita correttamente e va reinstallata.

Compatibilità punte: un vantaggio pratico (e economico)

  • Il TS101 è compatibile con le punte TS100: se avete già punte, le riusate; se dovete comprarle, trovate molta scelta.

La custodia rugged stampata in 3D

Se il saldatore è portatile, il punto debole diventa il trasporto: punta, cavo, stagno, spugnetta/lanetta… tutto deve essere protetto e ordinato.

Un contenitore stampato 3D risulta molto utile, ciò evita di utilizzare la scatola di cartone con cui vi viene venduto il saldatore. Il contenitore che ho stampato è una custodia rugged multi-scomparto, pensata specificamente per TS100/TS101, e derivata (remix) da un progetto precedente che trovate seguendo il link allegato.

Su Makerworld trovate molti contenitori simili a quello che sto utilizzando io ma questa soluzione mi piace perché:

  • può ospitare TS100/TS101
  • cavo
  • rocchetto stagno
  • lana metallica per pulizia punta
  • stand/rest (con cuscinetto 608, usato come appoggio per il saldatore caldo)
  • vani extra per piccoli accessori/ricambi

riassumendo una configurazione minimalista come piace a me 🙂

Buon Making a tutti 🙂

Arduino UNO Q: guida introduttiva (caratteristiche, pinout, esempi)

Sono passati circa quattro mesi dall’acquisizione di Arduino da parte di Qualcomm e dall’arrivo sul mercato di Arduino UNO Q. In questo periodo diversi lettori mi hanno scritto chiedendomi una presentazione più approfondita della scheda e un primo tutorial d’uso.
Va detto che negli ultimi anni Arduino ha fatto un grande passo avanti sulla qualità della documentazione: sul sito ufficiale trovate già molte informazioni e riferimenti utili. Come faccio spesso, però, durante i miei test e soprattutto quando devo preparare materiali per la didattica, preferisco raccogliere, riorganizzare e riformulare i contenuti provenienti da Arduino.cc e dalla documentazione tecnica, così da trasformarli in un percorso più chiaro, progressivo e “a misura di studente”.

Il formato di Arduino UNO Q resta quello “storico” della famiglia UNO, quindi compatibile come ingombri e disposizione generale, ma l’hardware introduce funzionalità insolite per questa categoria di schede. L’elemento distintivo è la presenza di due mondi in uno: da un lato un microcontrollore, dall’altro un microcomputer capace di eseguire Linux. I due sottosistemi sono pensati per dialogare con facilità, aprendo scenari interessanti per progetti IoT e applicazioni più evolute.

Specifiche tecniche

Arduino UNO Q unisce un microcontrollore STM32U585 e un microcomputer Linux basato su Qualcomm Dragonwing QRB2210, mantenendo l’impronta e il layout tipico delle schede Arduino.

Qualcomm Dragonwing™ QRB2210:

  • CPU quad-core Arm® Cortex®-A53 fino a 2,0 GHz
  • GPU Adreno per accelerazione grafica 3D
  • Doppio ISP (13 MP + 13 MP oppure 25 MP) fino a 30 fps

MCU STM32U585 Arm® Cortex®-M33 a 32 bit:

  • Core Arm® Cortex®-M33 fino a 160 MHz
  • 2 MB di memoria Flash
  • 786 kB di SRAM
  • FPU (unità di calcolo in virgola mobile)

Programmare la scheda Arduino UNO Q

Dal punto di vista della programmazione, UNO Q può essere usata come un Arduino tradizionale per gestire i GPIO tramite Arduino IDE, oppure si può sfruttare Arduino App Lab per lavorare in modo integrato sia sul microcontrollore sia sul microcomputer.

La parte MCU si sviluppa in C/C++ (come su molte altre schede Arduino), mentre sulla parte Linux è possibile creare ed eseguire script e programmi in Python.

Anche se la programmazione “a doppio binario” è una delle novità più interessanti, nulla vieta di usare la scheda in modo più semplice: solo MCU (come un Arduino classico) oppure solo Linux (come una single-board computer in stile Raspberry Pi).

In pratica, i possibili approcci sono:

  • Arduino classico: utilizzo del solo MCU;
  • App Lab integrato: ambiente unico con sketch C/C++ (MCU) e programmi Python (Linux/MPU);
  • Linux standalone: uso della scheda come computer Linux, con accesso diretto (ad esempio via SSH) o con periferiche collegate.


Continua a leggere

Mini progetto “dado elettronico” – due soluzioni

Ricordate la lezione: “randomSeed() su Arduino come usarla“. Vi avevo proposto un semplice esercizio sulla realizzazione di un dado digitale che genera un numero casuale da 1 a 6.

Vi propongo le due soluzioni:

  1. Con pulsante su pin digitale (lancio “fisico”)
  2. Con input da Serial Monitor (lancio “software”, immediato)

Entrambe sono adatte sia a Arduino UNO R3 sia a Arduino UNO R4.

Riprendiamo i concetti chiave visti nella lezione su randomSeed():

random(1,7) genera un numero pseudo-casuale tra 1 e 6.
Per evitare che la sequenza sia sempre uguale dopo un reset, conviene inizializzare il generatore con randomSeed(…). Nel dado, la cosa migliore è legare il seed a qualcosa di “imprevedibile”, ad esempio il momento in cui premete un pulsante (tempo umano) oppure un input seriale.

Versione A: dado con pulsante (pin digitale)

Materiale

  • 1 × Arduino UNO R3 o UNO R4
  • 1 × pulsante
  • cavetti e breadboard

Collegamenti

Usiamo la resistenza di pull-up interna, quindi niente resistenze esterne.

  • un piedino del pulsante -> GND
  • l’altro piedino del pulsante -> D2

Nel codice imposteremo pinMode(2, INPUT_PULLUP): a riposo il pin legge HIGH, quando premete legge LOW, ciò permette un cablaggio più semplice e rapido.

Nello sketch che segue imposteremo

  • debounce software (anti-rimbalzo)
  • seed inizializzato alla prima pressione usando micros() (molto efficace perché dipende dal tempo umano)
/*
  Prof. Maffucci Michele
  15.02.26
  Dado elettronico 1..6 con pulsante su D2 (INPUT_PULLUP).
  Include debounce e inizializzazione randomSeed al primo lancio.
*/

const byte PIN_PULSANTE = 2;

const unsigned long DEBOUNCE_MS = 30;

bool seedInizializzato = false;

int ultimoStatoLetto = HIGH; // con pull-up: HIGH a riposo
int statoStabile = HIGH;
unsigned long ultimoCambio = 0;

void setup() {
  Serial.begin(9600);
  delay(1000);
  pinMode(PIN_PULSANTE, INPUT_PULLUP);

  Serial.println("Dado elettronico (pulsante su D2).");
  Serial.println("Premi il pulsante per lanciare.\n");
}

void loop() {
  int lettura = digitalRead(PIN_PULSANTE);

  // Rilevo cambiamenti (possibili rimbalzi)
  if (lettura != ultimoStatoLetto) {
    ultimoCambio = millis();
    ultimoStatoLetto = lettura;
  }

  // Se è passato il tempo di debounce, considero stabile
  if (millis() - ultimoCambio > DEBOUNCE_MS) {
    if (lettura != statoStabile) {
      statoStabile = lettura;

      // Evento: pressione (con pull-up la pressione è LOW)
      if (statoStabile == LOW) {
        lanciaDado();
      }
    }
  }
}

void lanciaDado() {
  // Seed al primo lancio: il tempo umano rende la sequenza non ripetibile
  if (!seedInizializzato) {
    unsigned long seme = micros() ^ (unsigned long)analogRead(A0); // A0 può anche essere scollegato
    randomSeed(seme);
    seedInizializzato = true;
  }

  int risultato = random(1, 7); // 1..6

  Serial.print("Lancio! Risultato: ");
  Serial.println(risultato);
}

Versione B: dado con Serial Monitor (input testuale)

Una volta caricato lo sketch procedete in questo modo:

  • aprite il Serial Monitor a 9600 baud
  • impostate “Invio” (newline) o “CR+LF”
  • digitate un qualsiasi tasto e premete invio -> il dado lancia
/*
  Prof. Maffucci Michele
  15.02.26
  Dado elettronico 1..6 con input da Serial Monitor.
  Scrivete un carattere e premete invio per lanciare.
*/

bool seedInizializzato = false;

void setup() {
  Serial.begin(9600);
  delay(1000);
  Serial.println("Dado elettronico (Serial Monitor).");
  Serial.println("Scrivi un carattere e premi INVIO per lanciare.");
  Serial.println("Esempio: scrivi 'l' e invia.\n");
}

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

    // (Opzionale) scarto newline e carriage return
    if (c == '\n' || c == '\r') return;

    if (!seedInizializzato) {
      // Seed legato al momento in cui arriva il comando (tempo umano)
      unsigned long seme = micros() ^ (unsigned long)analogRead(A0);
      randomSeed(seme);
      seedInizializzato = true;
    }

    int risultato = random(1, 7);

    Serial.print("Comando ricevuto: '");
    Serial.print(c);
    Serial.print("' -> Risultato dado: ");
    Serial.println(risultato);

    Serial.println();
  }
}

In questo sketch l’istante in cui l’utente invia un carattere non è prevedibile: questo rende il seed variabile e quindi la sequenza di random() non riparte “uguale” ad ogni reset.

Esercizi per gli studenti

Attività 01: verifica del seed

  • lanciate 10 volte, annota i risultati;
  • premete reset e riprovate:
    • con seed legato alla pressione/comando dovreste vedere sequenze diverse.

Attività 02: statistica veloce

  • lanciate 60 volte;
  • contate quante volte esce ogni faccia (1..6);
  • discussione: la distribuzione è “circa uniforme?” Perché non è perfetta su pochi lanci?

Attività 03: debug consapevole

modificate il codice per usare un seed fisso, ad esempio randomSeed(1234) e confrontate:

  • questo è utile nei test perché rende i risultati ripetibili.

Buon Coding a tutti 🙂

Multitasking con Arduino – lezione 1/3

Per chi lavora con Arduino da un po’ di tempo, prima o poi si imbatte in questa frase letta online: delay() è il “male assoluto” :-). In realtà non è delay() in sé il problema: il punto è l’abuso che spesso se ne fa. Ogni volta che lo inseriamo nel codice, Arduino si ferma per tutto il tempo indicato, smettendo di leggere pulsanti, aggiornare uscite, gestire sensori o comunicazioni. In progetti semplici può anche andare bene, ma appena aumentano le funzioni da gestire “in parallelo”, quel blocco diventa un collo di bottiglia che rende il comportamento poco reattivo e difficile da far crescere.

Quando iniziamo a realizzare progetti con più componenti elettronici che interagiscono (LED che lampeggiano, pulsanti, messaggi su seriale, sensori…), delay() diventa subito un problema: blocca tutto. Se mettete un delay(500), per mezzo secondo Arduino non fa nient’altro: non legge pulsanti, non aggiorna altre uscite, non gestisce eventi.

La soluzione classica è passare a una logica non bloccante usando millis() e dividendo il programma in task (piccole funzioni) che vengono richiamate continuamente nel loop().

Ho trattato più volte dell’uso del millis() su questo sito, vi rimando al post: “Guida all’uso di millis() – Lezione 1” in cui ne mostro l’utilizzo in diverse situazioni.

Nel primo esempio realizziamo 3 processi (task) simultanei:

  1. LED1 su D9: lampeggia ogni 500 ms;
  2. seriale: stampa un messaggio ogni 1000 ms;
  3. pulsante su D2: ad ogni pressione commuta (ON/OFF) LED2 su D8 con debounce.

Per i collegamenti seguire lo schema che segue.

  • LED1: anodo > D9, catodo > resistenza 220 Ω > GND
  • LED2: anodo > D8, catodo > resistenza 220 Ω > GND
  • Pulsante: un capo > D2, l’altro capo > GND

Nota importante: non inseriamo la resistenza di pull-up esterna da 10 kΩ, perché abilitiamo la resistenza di pull-up interna con INPUT_PULLUP, pertanto a riposo il pin legge HIGH, premuto legge LOW.

L’idea di base è che ogni task controlla il suo tempo, quindi useremo una variabile che memorizza l’ultimo istante in cui ha fatto qualcosa.
Quando millis() - ultimoTempo >= intervallo, allora esegue l’azione e aggiorna ultimoTempo.
Ciò permette di far funzionare il ciclo di loop di Arduino senza nessun blocco e nessun task blocca gli altri task.

/*
    Prof. Maffucci Michele
    16.02.26

    Multitasking senza delay()
    - LED su D9 lampeggia ogni 500 ms
    - Stampa su Serial ogni 1000 ms
    - Pulsante su D2 (INPUT_PULLUP) commuta un LED su D8
*/

// Pin hardware
#define PIN_LED_LAMPEGGIO 9
#define PIN_LED_PULSANTE  8
#define PIN_PULSANTE      2

// "Timestamp" (in millisecondi) dell'ultima esecuzione dei task temporizzati
unsigned long tempoPrecedenteLed    = 0;
unsigned long tempoPrecedenteSerial = 0;

// Intervalli di esecuzione dei task (in millisecondi)
const unsigned long INTERVALLO_LED    = 500;
const unsigned long INTERVALLO_SERIAL = 1000;

// Stato logico dei LED
bool statoLedLampeggio = LOW;   // LED su D9
bool statoLedPulsante  = LOW;   // LED su D8

void setup() {
  // Impostiamo i pin dei LED come uscite
  pinMode(PIN_LED_LAMPEGGIO, OUTPUT);
  pinMode(PIN_LED_PULSANTE, OUTPUT);

  // Pulsante con pull-up interna:
  // - a riposo legge HIGH
  // - premuto (verso GND) legge LOW
  pinMode(PIN_PULSANTE, INPUT_PULLUP);

  // Stato iniziale delle uscite (opzionale ma consigliato per chiarezza)
  digitalWrite(PIN_LED_LAMPEGGIO, statoLedLampeggio);
  digitalWrite(PIN_LED_PULSANTE,  statoLedPulsante);

  Serial.begin(9600);
}

void loop() {
  // Il loop richiama continuamente i task.
  // Ogni task decide autonomamente "se è il momento" di fare qualcosa.
  taskLampeggioLed();
  taskStampaSeriale();
  taskLeggiPulsante();
}

// -------------------------------
// TASK 1: Lampeggio LED (non bloccante)
// -------------------------------
void taskLampeggioLed() {
  // Se è passato almeno INTERVALLO_LED dall'ultima volta, invertiamo lo stato del LED
  if (millis() - tempoPrecedenteLed >= INTERVALLO_LED) {
    tempoPrecedenteLed = millis();              // aggiorno l'istante di riferimento
    statoLedLampeggio = !statoLedLampeggio;     // toggle
    digitalWrite(PIN_LED_LAMPEGGIO, statoLedLampeggio);
  }
}

// -------------------------------
// TASK 2: Stampa su Serial (non bloccante)
// -------------------------------
void taskStampaSeriale() {
  // Stampa un messaggio ogni INTERVALLO_SERIAL millisecondi
  if (millis() - tempoPrecedenteSerial >= INTERVALLO_SERIAL) {
    tempoPrecedenteSerial = millis();
    Serial.println("Multitasking: 3 task attivi (senza delay)!");
  }
}

// -------------------------------
// TASK 3: Lettura pulsante + debounce + commutazione LED2
// -------------------------------
void taskLeggiPulsante() {
  // Variabili statiche: mantengono il valore tra una chiamata e la successiva.
  // Servono perché questa funzione viene richiamata di continuo nel loop().
  static bool ultimaLettura = HIGH;                 // ultima lettura "grezza" (non filtrata)
  static bool statoPulsanteStabile = HIGH;          // stato stabilizzato dopo debounce
  static unsigned long ultimoCambio = 0;            // quando è cambiata l'ultimaLettura

  const unsigned long RITARDO_DEBOUNCE = 50;        // tempo minimo per considerare stabile un cambio

  bool letturaAttuale = digitalRead(PIN_PULSANTE);  // lettura istantanea (può "rimbalzare")

  // Se la lettura è cambiata rispetto a prima, resettiamo il timer di debounce
  if (letturaAttuale != ultimaLettura) {
    ultimoCambio = millis();
    ultimaLettura = letturaAttuale;
  }

  // Se la lettura resta invariata per almeno RITARDO_DEBOUNCE ms,
  // possiamo considerarla stabile e aggiornare lo stato "ufficiale" del pulsante.
  if (millis() - ultimoCambio >= RITARDO_DEBOUNCE) {
    // Se lo stato stabile del pulsante è cambiato davvero...
    if (letturaAttuale != statoPulsanteStabile) {
      statoPulsanteStabile = letturaAttuale;

      // Con INPUT_PULLUP: premuto = LOW
      // Eseguiamo la commutazione SOLO sul fronte di pressione (quando diventa LOW).
      if (statoPulsanteStabile == LOW) {
        statoLedPulsante = !statoLedPulsante;                 // toggle LED2
        digitalWrite(PIN_LED_PULSANTE, statoLedPulsante);
      }
    }
  }
}
  • taskLampeggioLed()
    Controlla il tempo con millis(). Quando sono passati 500 ms, inverte lo stato del LED su D9.
  • taskStampaSeriale()
    Ogni 1000 ms stampa un messaggio. Anche qui: nessun delay(), quindi non blocca la lettura del pulsante.
  • taskLeggiPulsante()
    Legge D2 con pull-up interna. Implementa un debounce software:

    • registra quando la lettura cambia,
    • aspetta 50 ms di stabilità,
    • considera valido il cambio e, solo quando il pulsante viene premuto (LOW), commuta LED2.

Realizziamo ora una seconda versione dello sketch in cui:

  • Task 4: “fade” PWM non bloccante su un pin PWM (D10).
  • Stampa su Serial dello stato di LED2 (D8) ogni volta che viene commutato dal pulsante.

Collegamento aggiuntivo: un terzo LED (con resistenza 220 Ω) su un pin PWM (quelli con il simbolo tilde ~ sulla scheda). Nell’esempio utilizzo D10.

/*
    Prof. Maffucci Michele
    16.02.26

    Multitasking senza delay()
    - Task 1: LED su D9 lampeggia ogni 500 ms
    - Task 2: Stampa su Serial ogni 1000 ms
    - Task 3: Pulsante su D2 (INPUT_PULLUP) commuta LED su D8 + stampa stato
    - Task 4: Fade PWM su un pin PWM (es. D10) senza bloccare nulla
*/


// Pin hardware
#define PIN_LED_LAMPEGGIO   9     // LED1
#define PIN_LED_PULSANTE    8     // LED2
#define PIN_PULSANTE        2     // Pulsante verso GND (INPUT_PULLUP)
#define PIN_LED_FADE_PWM   10     // LED3 (serve un pin PWM: ~)

// Timestamp (millis) per i task temporizzati
unsigned long tempoPrecedenteLed     = 0;
unsigned long tempoPrecedenteSerial  = 0;
unsigned long tempoPrecedenteFadePwm = 0;

// Intervalli (ms)
const unsigned long INTERVALLO_LED     = 500;
const unsigned long INTERVALLO_SERIAL  = 1000;
const unsigned long INTERVALLO_FADEPWM = 10;   // velocità aggiornamento fade (più basso = più fluido/rapido)

// Stati
bool statoLedLampeggio = LOW;   // LED su D9
bool statoLedPulsante  = LOW;   // LED su D8

// Parametri fade PWM (0..255 su molte schede Arduino)
int  luminositaPwm = 0;         // livello corrente
int  passoPwm      = 5;         // quanto cambia ad ogni aggiornamento (es. 1..10)
bool aumentaPwm    = true;      // direzione del fade

void setup() {
  pinMode(PIN_LED_LAMPEGGIO, OUTPUT);
  pinMode(PIN_LED_PULSANTE,  OUTPUT);

  // Pull-up interna: a riposo HIGH, premuto LOW (pulsante collegato a GND)
  pinMode(PIN_PULSANTE, INPUT_PULLUP);

  // Pin PWM: lo usiamo con analogWrite() (non serve pinMode obbligatorio, ma lo mettiamo per chiarezza)
  pinMode(PIN_LED_FADE_PWM, OUTPUT);

  // Stato iniziale delle uscite
  digitalWrite(PIN_LED_LAMPEGGIO, statoLedLampeggio);
  digitalWrite(PIN_LED_PULSANTE,  statoLedPulsante);
  analogWrite(PIN_LED_FADE_PWM,   luminositaPwm);

  Serial.begin(9600);
}

void loop() {
  // Ogni task viene richiamato continuamente.
  // Nessuno usa delay(): ogni task decide da solo quando agire.
  taskLampeggioLed();
  taskStampaSeriale();
  taskLeggiPulsante();
  taskFadePwm();
}

// -------------------------------
// TASK 1: Lampeggio LED (non bloccante)
// -------------------------------
void taskLampeggioLed() {
  if (millis() - tempoPrecedenteLed >= INTERVALLO_LED) {
    tempoPrecedenteLed = millis();
    statoLedLampeggio = !statoLedLampeggio;
    digitalWrite(PIN_LED_LAMPEGGIO, statoLedLampeggio);
  }
}

// -------------------------------
// TASK 2: Stampa su Serial (non bloccante)
// -------------------------------
void taskStampaSeriale() {
  if (millis() - tempoPrecedenteSerial >= INTERVALLO_SERIAL) {
    tempoPrecedenteSerial = millis();
    Serial.println("Multitasking cooperativo: 4 task attivi (senza delay)!");
  }
}

// -------------------------------
// TASK 3: Lettura pulsante + debounce + commutazione LED2
//       + stampa dello stato di LED2 quando cambia
// -------------------------------
void taskLeggiPulsante() {
  static bool ultimaLettura = HIGH;            // lettura grezza precedente
  static bool statoStabile  = HIGH;            // stato stabilizzato (debounce)
  static unsigned long ultimoCambio = 0;       // momento dell'ultimo cambio lettura

  const unsigned long RITARDO_DEBOUNCE = 50;

  bool letturaAttuale = digitalRead(PIN_PULSANTE);

  // Se la lettura grezza cambia, aggiorniamo il timer di debounce
  if (letturaAttuale != ultimaLettura) {
    ultimoCambio = millis();
    ultimaLettura = letturaAttuale;
  }

  // Se la lettura resta stabile per un tempo sufficiente, la accettiamo come valida
  if (millis() - ultimoCambio >= RITARDO_DEBOUNCE) {
    if (letturaAttuale != statoStabile) {
      statoStabile = letturaAttuale;

      // Con INPUT_PULLUP: premuto = LOW
      // Commutiamo LED2 solo sul fronte di pressione (quando diventa LOW)
      if (statoStabile == LOW) {
        statoLedPulsante = !statoLedPulsante;
        digitalWrite(PIN_LED_PULSANTE, statoLedPulsante);

        // Stampa su Serial dello stato di LED2 al momento della commutazione
        Serial.print("LED2 (D8) ora e': ");
        Serial.println(statoLedPulsante ? "ACCESO" : "SPENTO");
      }
    }
  }
}

// -------------------------------
// TASK 4: Fade PWM (non bloccante) su pin PWM
// -------------------------------

void taskFadePwm() {
  // Aggiorniamo la luminosità con una cadenza regolare (INTERVALLO_FADEPWM).
  // Così l'effetto è fluido e, soprattutto, non blocca gli altri task.
  if (millis() - tempoPrecedenteFadePwm >= INTERVALLO_FADEPWM) {
    tempoPrecedenteFadePwm = millis();

    // Aggiorna luminosità in base alla direzione
    if (aumentaPwm) {
      luminositaPwm += passoPwm;
      if (luminositaPwm >= 255) {             // limite alto (classico 8-bit)
        luminositaPwm = 255;
        aumentaPwm = false;                   // inverti direzione
      }
    } else {
      luminositaPwm -= passoPwm;
      if (luminositaPwm <= 0) {               // limite basso
        luminositaPwm = 0;
        aumentaPwm = true;                    // inverti direzione
      }
    }

    // Applica PWM (luminosità percepita del LED)
    analogWrite(PIN_LED_FADE_PWM, luminositaPwm);
  }
}

Il fade PWM è un esempio perfetto per capire il multitasking su Arduino, perché l’effetto sembra continuo e fluido, quasi come se Arduino stesse “dimmerando” il LED in tempo reale senza occuparsi d’altro. In realtà non c’è nessuna operazione continua: il programma aggiorna la luminosità a piccoli passi (ad esempio +5 o −5) a intervalli regolari (per esempio ogni 10 ms), usando millis() per decidere quando fare l’aggiornamento successivo. Tra un passo e l’altro il loop() continua a girare e può eseguire gli altri task (lampeggio, lettura pulsante, seriale). È proprio questa alternanza rapida di micro-azioni temporizzate che crea l’illusione del “continuo”, mantenendo però il codice reattivo e capace di gestire più funzioni “in parallelo” senza blocchi e senza delay().

Buon Making a tutti 🙂

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 🙂