Ciclo while (alle prime armi)

Buona sera a tutti.

Siccome è da poco che mi sono approcciato ad Arduino ovviamente saltano fuori alcuni problemi ed incertezze..

Sto cercando di capire al meglio il funzionamento del ciclo "while", che in tutta sincerità il funzionamento l'ho anche capito ma non mi è ben chiaro come applicarlo.

Ho voluto creare un piccolo e semplice sketch per vedere se avessi capito come usarlo, in questo caso in particolare per accendere e spegnere 3 led e al premere di un pulsante essi smettono di accendersi, una volta lasciato il pulsante il ciclo dovrebbe riprendere.

So bene che ci sono altri modi e forse anche più semplici per avere questo risultato ma era un "esercizio" mio di prova per capire se può essere applicato anche in una situazione come questa.

Vi chiedo gentilmente il vostro aiuto, se posso avere dei consigli su come sistemare lo sketch ed eventualmente anche spiegare perché e dove ho sbagliato, così da farne tesoro e imparare qualcosa di nuovo.

Vi lascio in allegato lo sketch.

PS: non ci sono commenti nello sketch perché è uno sketch molto semplice e in più vi ho descritto di cosa si tratta, spero che questo non crei problemi.

Grazie mille.

Gio.

#define led1 11
#define led2 12
#define led3 13
#define tasto 8



void setup() {
  pinMode(led1, OUTPUT);
  pinMode(led2, OUTPUT);
  pinMode(led3, OUTPUT);
  pinMode(tasto, INPUT);
}

void loop() {
  bool run = true;
  while (run) {
    ciclo();
  }
  if (digitalRead(tasto) == HIGH) {
    run = false;
  }
}

void ciclo() {
  digitalWrite(led1, HIGH);
  delay(200);
  digitalWrite(led1, LOW);
  delay(200);
  digitalWrite(led2, HIGH);
  delay(200);
  digitalWrite(led2, LOW);
  delay(200);
  digitalWrite(led3, HIGH);
  delay(200);
  digitalWrite(led3, LOW);
  delay(200);
}

Si, serve fare tanta pratica per scoprire quando usare while, do while e quando invece sfruttare l'unico ciclo infinito predefinito, cioè la funzione loop(), la quale inizia, termina e ricomincia all'infinito.

Ora tu vorresti che la funzione ciclo() venisse chiamata in un while() fintanto che la variabile run risulta vera (true). Allora devi spostare la dichiarazione della variabile da locale a globale così che conservi il valore:

bool run = true;  // Variabile run dichiarata globale.
void setup() {
}

void loop() {
    while (run) {
// ecc.

Molto spesso il codice all'interno del while modifica la variabile run. Il while lo si usa spesso con gli array di caratteri detti C string o comunque per iterare per un certo numero di cicli. Di seguito ti lascio un link ad una articolo dove si spiega come realizzare una scritta scorrevole, esso contiene un while. Nota il codice è complesso ma può aiutarti a prendere spunto anche su altro, ad esempio millis().

Ciao.

1 Like

I cicli bloccanti come il while vanno trattati con i guanti bianchi soprattutto tenendo in considerazione che la funzione loop() è di fatto un "super while".
Io tendenzialmente cerco di evitarli quando possibile perché è molto facile creare delle situazioni di "deadlock".

Nel tuo sketch ci sono essenzialmente 2 problemi gravi:

  • la variabile bool run l'hai dichiarata locale e ad ogni iterazione del ciclo loop() essa sarà creata nuovamente da zero ed inizializzata a true. Quindi in sostanza è sempre vera quando vai a testarla nel while.
  • fintanto che run == true il micro eseguirà soltanto le istruzioni racchiuse nelle parentesi delimitate dal while. L'if successivo non verrà mai valutato, quindi non esci mai dal while.

Quello che hai scritto in termini di funzionamento è equivalente a:

void loop() {  
    ciclo();  
}

Per ottenere quello che desideri, dovresti dichiarare la variabile run come static o più semplicemente dichiararla a livello globale e spostare l'if all'interno del ciclo while.

1 Like

Una possibile soluzione per il tuo esercizio, usando il while():

void loop() {
   ciclo();
   while (digitalRead(tasto) == HIGH) {
    delay(100);
  }
}

Tieni conto che il pulsante non sarà sempre immediatamente reattivo, a causa dei delay() usati nel ciclo().

Ciao, Ale.

1 Like

In realtà dato che, come ti hanno detto, la funzione "loop()" è già un ciclo while infinito, non serve quasi mai usare anche dei while() interni.
Nel tuo caso poi, ignorando eventuali "bounce" del pulsante (ed assumendo che tu abbia messo una resistenza pull-down ossia dal pin a GND) basterebbe fare tutto con una sola riga nel loop():

void loop() {
  if (digitalRead(tasto) == LOW) ciclo();
}

Ma, debounce a parte, il problema è che la tua loop() contiene parecchi delay, per cui qui la cosa si dovrebbe gestire diversamente, usando la funzione millis(), che ti consiglio di iniziare a vedere prima possibile, perché è essenziale nella programmazione di microcontroller come questo, visto anche che non hanno che un solo thread.
Per ora restiamo su loop() e while(), poi andremo avanti. :wink:

1 Like

Ti ringrazio per i preziosi consigli ed il link associato che ben presto guarderò e studierò attentamente.

Un caro saluto.

Gio.

Mi hai aiutato molto facendomi notare miei due errori, li terrò a mente per i prossimi progetti.

Grazie mille.

Proverò sicuramente lo sketch che mi hai descritto.

Grazie.

Wow molto interessante la sintassi che mi hai descritto, semplice e sicuramente funzionale.
Riguardo ai millis(); ci arriverò molto presto! Ho preso un libro "Arduino la guida essenziale 2.0" e lo so leggendo tutte le sere e ho già notato che c'è la spiegazione su millis(); la studierò con molta attenzione.

Intanto ti ringrazio tanto per i consigli e il tempo dedicatomi.

Gio.

Ti aggiungo anche un altro paio di consigli.

Primo, per sperimentare e fare pratica è comodissimo usare un emulatore online con WokWi. Creando un account (gratuito) ti permette di "disegnare" il circuito visto che ha vari elementi disponibili (oltre a vari Arduino ci sono resistenze, LED, potenziometri, eccetera), quindi a meno di progetti con componenti non molto comuni, puoi usarlo. Innanzitutto per avere tu stesso uno schema, che puoi persino catturare come immagine e condividere, fino a condividere direttamente il link al tuo progetto (chi lo apre potrà solo copiarlo nel proprio spazio prima di modificarlo).
Non solo, puoi caricare il codice e provarlo direttamente, quindi senza toccare neanche un cavo né saldatore. Una volta che tutto funziona, puoi creare il vero prototipo (cosa facilitata proprio dal fatto di avere uno schema completo).
Te lo consiglio caldamente perché si impare molto più rapidamente.

Altro consiglio, più che altro "stilistico". Per convenzione, i valori costanti (variabili "const" e simboli "#define") si mettono tutti in maiuscolo, non è un errore non farlo ma è utile per poterli distinguere ad occhio dalle variabili. Inoltre quando definiscono dei pin, io preferisco aggiungere un prefisso "P_" (quindi non "led1", ma "P_LED1") per evidenziare che quel valore rappresentato dal simbolo è un numero di pin:

#define P_LED1 11

Puoi anche usare variabili costanti ("const") che puoi dichiarare come "int" ad esempio oppure, nel caso dei pin, va benissimo byte visto che il numero pin non super mai 255:

const byte P_LED1 = 11;

Ultimo consiglio "di base": per i pulsanti, la soluzione più semplice è in genere usare INPUT_PULLUP, per evitare di dover sempre mettere una resistenza esterna (pullup o pulldown, spero che tu nel tuo circuito l'abbia messa) perché in questo modo attivi la resistenza interna di Arduino:

  pinMode(tasto, INPUT_PULLUP);

A quel punto devi solo ricordarti che il pulsante premuto va LOW mentre se non è premuto sarà HIGH.

Nel frattempo comunque il codice che hai non è molto pratico perché controlli la pressione del pulsante solamente una volta ogni 1.2 secondi! Teoricamente dovresti prevedere l'uso di millis() al posto dei delay(), semplicementee memorizzando in una variabile lo "stato" del sistema, inteso ad esempio come un valore che indica quale led è attualmente acceso:
0 = tutti i led spenti
1 = led 1 acceso
2 = led 1 spento
3 = led 2 acceso
4 = led 2 spento
5 = led 3 acceso
6 = led 3 spento
Quindi nel tuo codice se il tasto è premuto, attendi che venga rilasciato. Quindi ogni 200 millisecondi devi controllare lo "stato":
se è 0, attivi il led 1 e passi allo stato 1;
se è 1, spegni il led 1 e passi allo stato 2;
se è 2, attivi il led 2 e passi allo stato 3;
se è 3, spegni il led 2 e passi allo stato 4;
se è 4, attivi il led 2 e passi allo stato 5;
se è 5, spegni il led 1 e passi allo stato 6;
se è 6, attivi il led 2 e passi allo stato 0;

Quindi devi creare una variabile globale inter "stato" dandogli il valore iniziale 0. Per l'uso di millis() devi solo memorizzare in una variabile globale (ossia accessibile a tutto il codice) il suo valore corrispondente al precedente evento.
Ti faccio un esempio, non ti voglio dare la "pappa pronta" quindi ti dò solo un codice parziale:

#define P_LED1 11
#define P_LED2 12
#define P_LED3 13
#define P_TASTO 8

#define INTERVALLO 200

int stato = 0;
unsigned long tmrStato = 0;

void setup() {
... modifica tu i pinMode...
  
  tmrStato = millis();
}

void loop() {
  if (digitalRead(P_TASTO) == LOW) {    
    delay(50); // minimo debounce    
    while (digitalRead(P_TASTO) == LOW)
      delay(50); // attendi che venga rilasciato il tasto
  }
  ciclo();
}

void ciclo() {
  if (millis() - tmrStato >= INTERVALLO) {
    tmrStato = millis();
    switch (stato) {
      case 0:
        digitalWrite(P_LED1, HIGH);
        stato = 1;
        break;
      case 1:
        digitalWrite(P_LED1, LOW);
        stato = 2;
        break;
... eccetera, prosegui tu ...
    }
  }
}

Se ti va, prova tu a completare il codice e vedere come funziona.

Buongiorno caro docdoc.

Sono veramente sbalordito dall'aiuto che mi sta dando e dei preziosi consigli da te citati.

Ne studierò attentamente la loro funzione e il loro significato e farò pratica col piccolo programma di codice che mi ha allegato provandolo a completare.

Un immenso grazie.

Gio.

Riguardo alla variabile millis(); dopo varie letture del codice che mi hai scritto ho capito la sintassi e la sua funzione, ma comunque continuerò ad approfondire sul discorso millis();.

Riguardo al resto del codice non mi e chiaro nella funzione loop perché viene inserito un if e a seguire il ciclo while.. non capisco come interpretare il codice..

E nella sezione switch(); lo stato come fa ad incrementare se in nessuna parte del codice si incrementa lo stato con stato++?

Spero di essermi espresso al meglio.

La if() fa entrare in quel codice in attesa che il tasto venga rilasciato, il while() fa proprio questo: nel tuo caso dato che vuoi esplicitamente fermare l'esecuzione quando viene premuto il pulsante. Normalmente non si fa così ma per questo sketch è sufficiente. In generale si potrebbe fare la stessa cosa in modo diverso, ad esempio così:

...
bool running = true;
...
void loop() {
  if (running && digitalRead(P_TASTO) == LOW) {    
    // impedisce l'esecuzione
    running = false;
    delay(50); // minimo debounce    
  } else {
    running = true;
    ciclo();
  }
  // Qui può fare altre cose...
}
...

La differenza è che qui il loop() non viene bloccato, e se dopo la if() si devono gestire altre cose, lo si può fare.
Non è necessario per ora, ma è una cosa da iniziare a saper considerare e gestire.
Ma sei riuscito a completare il tuo codice con i miei consigli? Se si, posta il nuovo codice.

Non si incrementa, ma si imposta direttamente al valore di stato successivo:

    switch (stato) {
      case 0:
        digitalWrite(P_LED1, HIGH);
        stato = 1;
        break;

Ovviamente qui gli stati hanno codice sequenziale, quindi dallo stato 0 si va sempre allo stato 1 e così via (e dopo l'ultimo ricorda che devi tornare allo stato 0!), ma in generale non significa che non si possa avere un flusso diverso.

Per darti una prima "infarinata", sappi che questa cosa è un primo "embrione" di ciò che si chiama "macchina a stati finiti", o "FSM" (dall'inglese "Finite States Machine"). Quando si "disegna" una FSM si usano dei cerchi per gli stati, per i quali si usa un simbolo e/o un numero, ed ogni transizione da uno stato ad un altro è identificato da un arco con freccia il quale è associato ad una condizione ed una eventuale azione, e, quindi, lo stato di destinazione. Uno di questo stati viene marcato come quello iniziale (es. "START").

Si può fare disegnando a mano su un foglio questo schema, e poi "traducendolo" in codice, ma per farlo in modo testuale si può scrivere ogni stato con questi campi:

Stato:
Evento:
Azione:
Prossimo:

Ad esempio diciamo di voler accendere un led quando si preme il tasto e spegnerlo quando lo si preme nuovamente:

Stato: START
Evento: tasto premuto
Azione: accendi led
Prossimo: ATTESA1

Stato: ATTESA1
Evento: tasto rilasciato
Azione: -nessuna-
Prossimo: ACCESO

Stato: ACCESO
Evento: tasto premuto
Azione: spegni led
Prossimo: ATTESA2

Stato: ATTESA2
Evento: tasto rilasciato
Azione: -nessuna-
Prossimo: START

A questo punto se associ un valore numerico ad ogni stato (oppure, meglio, definisci delle costanti con il nome dello stato) la gestione si fa con uno "switch()" sullo stato, come fatto ora, poi l'evento corrisponderà ad una "if()" dentro al "case" di quello stato, e per cambiare stato si assegna semplicemente il valore dell'altro codice.

Considera che sia millis() (quindi fare in modo che il "loop()" non si fermi, ossia evitare l'uso di "delay()", per poter fare più cose "contemporaneamente") sia FSM (quindi anche con l'istruzione "switch()") sono alla base di gran parte dei codici di un microcontrollore come Arduino, per cui è bene che tu li conosca.

Se vuoi fare un po' di pratica prova a fare un tuo sketch che implementa questo che ti ho descritto, magari usando WokWi così fai esperimenti molto rapidamente, e facci sapere se riesci, anche qui, posta il tuo codice così ti posso eventualmente dare qualche altro consiglio.

Ciao Doc e grazie ancora dei tuoi consigli, li apprezzo davvero molto.

si if,switch,while e millis sono quelli sulla quale mi sto applicando di piu per impararli al meglio o almeno, per usarli bene..

ti passo il codice che ho finito continuando quello che mi ai scritto tu, funziona e spero che non ci siano errori.

grazie ancora.Switch

Apparentemente si, ma prova a tenere premuto il tasto e vedi cosa succede...

Il fatto è che non hai ben letto quello che ti avevo scritto a proposito dei pullup/pulldown sui pulsanti, perché nello schema hai messo il pulsante con una resistenza di pull-down (ossia tra pin e GND) e lo hai inizializzato come INPUT:

...
  pinMode(P_TASTO, INPUT);  
  tmrStato = millis();
...
void loop() {
  if (digitalRead(P_TASTO) == LOW) {    
    Serial.println("tasto");
    delay(50); // minimo debounce    
    while (digitalRead(P_TASTO) == LOW)
      delay(50); // attendi che venga rilasciato il tasto
  }
...

per cui "digitalRead(P_TASTO)" restituisce sempre LOW se non viene premuto, col risultato che nel loop() entra subito nella if() e da qui resta per sempre dentro alla "while()" fino a che non premi il pulsante (che, con quello schema, diventa HIGH). E di fatto il codice è bloccato lì e non potrà mai fare null'altro vanificando anche l'uso di millis().

Quando si usano pulsanti bisogna sempre decidere se usare una resistenza esterna di pull-down (come hai fatto tu) e quindi il tasto premuto sarà HIGH, oppure usare il pullup interno definendo il pin INPUT_PULLUP e quindi il tasto premuto sarà LOW. Se metti la resistenza esterna devi fare:

  if (digitalRead(P_TASTO) == HIGH) {    
    delay(50); // minimo debounce    
    while (digitalRead(P_TASTO) == HIGH)
      delay(50); // attendi che venga rilasciato il tasto
  }

oppure lasciare LOW, togliere la resistenza esterna e definire il pin:

  pinMode(P_TASTO, INPUT_PULLUP);

Ah, poi comunque abituati a mantienere l'indentazione, hai alcune istruzioni da spostare un poco (nell'IDE fai Ctrl-T e te lo sistema lui tutto).

Detto questo, i vantaggi di questa struttura con una macchina a stati finiti si possono vedere se ad esempio invece di farli accendere in quella sequenza tu volessi farli accendere in una sequenza diversa, diciamo invece del primo, secondo e terzo led vuoi fare primo, terzo, e secondo. Ti basta cambiare le transizioni tra stati ossia le righe ""stato = ".

Ma ovviamente da ora in avanti non solo potresti far fare anche altro "contemporaneamente" (lo fa molto velocemente, diciamo nello stesso ciclo), ma anche implementare altre funzionalità (a patto che "disegni" sempre in questo modo gli stati e relative transizioni).

1 Like

Buongiorno doc.

Diamine mi ero scordato del pullup del tasto! In effetti ho modificato anche nel loop lo stato di LOW a HIGH per modificarne l'effetto!

Mi sto applicando molto sul discorso macchina a stati finiti e ho provato a fare varie prove con buoni risultati e anche usando la funzione millis e tutto grazie anche ai tuoi consigli, ti ringrazio ancora per l'aiuto che mi hai dato.

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.