Menù con LCD 4x20 ed encoder

una domanda, alla fine di ogni funzione, chiamate dal menù principale, ho messo:
Menu_principale();
Se non metto questa chiamata, l'esecuzione non dovrebbe tornare sempre alla routine Menu_principale, dato che le routine scelte vengonono chiamate da quella principale? Se tolgo la chiamata al menù principale, l'esecuzione va al loop.

Urca non mi ero accorto della chiamata ricorsiva.
Cioè Menu_principale() contiene le chiamate a funzione seguenti:

  1. prima_scelta()
  2. seconda_scelta()
  3. terza_scelta()
  4. quarta_scelta()

Ogni funzione detiene il controllo in un ciclo while sempre condizionato dalla variabile esci. Quando il while cessa di ciclare c'è una chiamata alla funzione Menu_principale().
Questa è codice con chiamate ricorsive che prima o poi portano al crash.

Devi evitare la ricorsione assolutamente. Io ho provato ad eliminare la chiamata e non funge più come mi aspetto.

Ciao.

Perchè dovrebbero portare al crash? A me sembra tutto logico, in ogni routine ci deve stare per forza un while, altrimenti come fai ad uscire dalla routine in base alla scelta?
Il menù principale è il codice a cui devono tornare tutte le altre routine.

Si lo vedo ora, scommettiamo che saltano fuori altri problemi, uno ad esempio dentro la funzione prima_scelta() c'è il case 0 chei chiama nuovamente la funzione prima_scelta().

Quando il while(esci == 0) {} di prima_scelta() termina perché ad esci è stato assegnato 1, anche la funzione termina e restituisce il controllo al chiamante che è il case 0 della funzione Menu_principale e dovrebbe rimanere dentro il while di questa funzione ma esci==0 per cui termina pure Menu_principale().

Le chiamate ricorsive portano al crash perché il chiamato non restituisce mai il controllo al chiamante. A chi deve restituire il controllo quando una funzione termina viene memorizzato nella ram.
Ad ogni chiamata viene salvato l'indirizzo del punto in cui deve ritornare l'esecuzione quando la funzione termina, ma qui non termina, per cui una nuova locazione di memoria viene impiegata per salvare il punto di ritorno che non avviene mai. Prima o poi la ram finisce.

Perché come già spiegato da Maurotec le chiamate a funzione non sono dei meri salti. Viene allocato spazio sullo stack non solo per l'indirizzo di ritorno, ma anche per tutte le variabili locali. Se da dentro una funzione richiami la stessa funzione, non la fai ripartire da capo, ma ne avvii un'altra copia (e la precedente rimane in sospeso occupando memoria).

Quello è il comportamento finale desiderato del sistema, che ovviamente deve essere quello, ma non è l'unico modo per implementarlo.

Esempio routine bloccante con while:

loop() {
  switch (scelta)
  {
      case 0: routine_a(); break;
      case 1: routine_b(); break;
      case 2: routine_c(); break;
  }
}

void routine_a() {
    while (1) {
        ...tante cose...
        if (...) { scelta=1; break; }
    }
}

Stessa routine non bloccante, dove invece di un while si sfrutta la "naturale" iterazione insita nel loop:

loop() {
  switch (scelta)
  {
      case 0: routine_a(); break;
      case 1: routine_b(); break;
      case 2: routine_c(); break;
  }
}

void routine_a() {
    ...tante cose...
    if (...) { scelta=1; return; }
}

Me ne sono accorto anche io, se vedi bene si trova in tutte e quattro le routine perchè avevo fatto un copia ed incolla del menù principale, rimettendo apposto ho risolto anche il problema dell'uscita della terza scelta.

Non ho capito bene come funzionano le routine.

Per esempio, nel menù principale io chiamo la routine stampa_freccia(), finisce di stampare e torna indietro andando alla routine leggi_Encoder(), una volta letto l'encoder torna indietro e prosegue con if.
Invece:

Se alla fine delle 4 routine di scelta, tolgo la chiamata alla routine Menu_principale(), non torna alla routine che l'ha chiamata (che sarebbe la routine Menu_principale), come nel caso precedente, ma va al loop principale, perchè?

Ci torna ci torna e devi verificarlo tu in prima persona.
Per verificarlo basta aggiungere un serial print nel case 0 dopo la chiamata a prima_scelta(). Il problema è che non rimane nel while perché esci vale 1.
Anzi puoi verificare che esci vale 1 stampando con serial print.

Sono parecchio arrugginito e quindi sto ripassando un poco di teoria dei nodi gerarchici. Un menu è solitamente rappresentabile come un albero in cui ci sono rami e foglie, il tronco dell'albero è l'antenato di tutti i rami e delle foglie. Una foglia non può avere figli e per questo diciamo che è un nodo terminale.

Se un elemento di un menu è un nodo questi li posso combinare in una struttura gerarchica ad albero. Ci sarà ovviamente un elemento terminale. Un elemento allora oltre a contenere delle informazioni come ad esempio il nome la descrizione ecc che consideriamo accessorie perché le informazioni fondamentali per combinarlo ad albero sono:

  1. chi è il padre?
  2. chi è il figlio?
  3. chi e il fratello?

Se è nodo terminale la risposta alla 2 sarà nessuno. Il nodo però ha di sicuro un padre e forse dei fratelli.

Il nodo principale non ha un padre ne fratelli ma solo figli ed eventualmente nipoti.

Ho altri libri sull'argomento e in genere qualunque libro su strutture dati e algoritmi può andare bene, anche se in alcuni libri l'argomento è trattato in modo teorico e pertanto non prende in considerazioni un linguaggio ma espone teoricamente l'argomento e formula degli algoritmi in pseudo code.

Mentre online ho trovato queste risorse:
Liste in C
Algoritmi e Strutture Dati

Per realizzare queste strutture gerarchiche per un nenu purtroppo io non la soluzione semplice in C. Il problema sono gli array, le struct, glia array di struct e i puntatori di vario tipo. Tutta roba che è complessa pure per me che sto cercando un modo per creare una struttura di menu a tempo di compilazione che sia flessibile, semplice da capire e usare. Evidentemente non l'ho ancora trovata.

Ritornando alla struttura gerarchica e leggendo quei due link @arturo2222 saresti capace di fare un disegno con la struttura gerarchica con i nomi dei tuoi menu. Chiedo perché non riesco a crearla io. Attualmente sono capace di creare una struttura di menu che lavora a runtime ma vorrei riuscire in qualche modo a crearla anche a compile-time proprio perché l'avevo creata anni addietro per un controller di gestione cella frigorifera che ha centinaia di dati configurabili e alcuni sono riservati e protetti da password. Inoltre era possibile prendere un parametro e spostarlo in un altro menu e ciò lo può fare l'utente attraverso 4 pulsati tattili e display a 7x4 segmenti. Per cui i nomi dei menu sono delle abbreviazioni di massimo tre caratteri come pure i parametri da potere configurare.

Se trovare risorse riguardo ai menu alla parte pratica e quella teorica postate pure.

Ciao.

Fatto, avevi ragione. Il problema era nella variabile globale esci, è bastato toglierla come globale e ho risolto. I link postati sono davvero interessanti, ma implica studiare nuove cose, a malapena riesco a fare cose semplici, voglio prima esser più sicuro sulle cose semplici e poi vado sulle complicate. Riguardo alla struttira gerarchica vedo cosa posso fare.

Spero che intendevi questo come schema

Ok, corretto il modo di procedere. Però una lettura superficiale devi dargliela alla teoria dei nodi perché sono concetti fondamentali che nella pratica aiutano tantissimo e le tante librerie per i menu usano questi concetti internamente.

Un esempio: Se tutti i nodi hanno le stesse proprietà una sola funzione sarà capace di lavorare con il nodo selezionato e se questo ha fratelli li visualizza e ti da la possibilità di selezionarne uno, dopo la selezione la stessa funzione farà ciò che ho descritto prima con il nuovo nodo selezionato. Quando selezioni il nodo Esci che è un nodo foglia il risultato che devi ottenere è quello di salire di un grado gerarchico, cioè risalire al padre di Esci. Il nodo MenuPrincipale ha solo figli e non ha un genitore o se vogliamo il genitore è il loop.

Tornando alla pratica e alle chiamate ricorsive che sono usate solo nel caso in cui la profondità della ricorsione non supera le capacità del sistema. Se cerchi "calcolo del fattoriale in C" trovi diversi articoli, qui ne linko uno solo:
https://www.andreaminini.com/programmazione/c/le-funzioni-ricorsive-in-c

Il sunto è che la ricorsione è una tecnica di programmazione elegante.

Non proprio, quel disegno è un diagramma a blocchi funzionali. Va bene comunque fare questi diagrammi, tranne qualche errore sul nome dei nodi che mi porta in confusione, credo di potere dire che il nodo "Scelta 1" ha 5 nodi figli di cui Esci è la foglia. Mentre "Scelta 2" ha 9 figli di cui Esci è il nodo foglia. "Scelta 1" ha 4 fratelli di cui Esci è la foglia. Ogni nodo foglia confermato ha come conseguenza di risalire al padre il quale mostra i suoi figli.

Ciao.

Perchè con "Scelta 1" hai fatto 2 diverse considerazioni?

Iniziando a leggere il tuo link, ho visto questa frase: Tuttavia bisogna però fare attenzione al numero di ricorsioni, perché a ogni ricorsione la funzione alloca più memoria per salvare le nuove variabili locali.
Nel mio listato avevo creato la variabile globale bool esci=0, però questo mi dava il problema visto sopra nel ritornare al menù principale. Togliendola come globale e creandola come locale in ogni routine, viene consumata più memoria, secondo quanto riportato dall'articolo.
Mi conviene rimetterla come gloabale e azzerarla alla fine di ogni routine prima che torna al menù principale, giusto?

Non sono 2 considerazioni separate. MenuPrincipale è il nome del padre che ha come figli: S1, S2, S3 ed S4. Il tipo di parentela tra questi nodi è che sono fratelli. Quando studierai le liste collegate ti renderai conto meglio della importanza delle relazioni, nella pratica il nodo S4 sa che S3 è suo fratello ma non sa della esistenza di S1 ed S2.

S3 invece sa che S2 ed S4 sono suoi fratelli ma nulla sa di S1.

Nella lista doppiamente collegata S1 sa di S2 e S4.

S4 sa di S3 e di S1.

No, quanto scritto nel link vale solo nel caso di ricorsione che purtroppo non è stata ben evidenziata da Minini. Qui c'è la chiamata ricorsiva:

return n * fattoriale(n-1);

Dove il comando return non viene eseguito perché prima viene fatta una chiamata a se stessa. Evidenzio ancora meglio la chiamata: fattoriale(n-1).

Se ci ragioniamo anche tra le funzioni c'è gerarchia:
MenuPrincipale è la funzione che ha come padre la funzione loop. MenuPrincipale ha dei figli ad esempio Scelta_1() la quale ha pure dei figli ad esempio Scelta_1_1(). Si ha ricorsione se Scelta_1_1() chiama uno dei suoi discendenti ad esempio se chiama Scelta_1() che è il padre oppure MenuPrincipale() che è il nonno.

Riguardo alla lista doppiamente collegata che hai già voluto implementare usando un array. Se l'indice i dell'array è minore di 0 i = n-1, dove n è il numero degli elementi dell'array. Se i > n-1 i = 0. Quindi colleghi l'elemento finale all'elemento iniziale e questo e collegato all'elemento finale.

Ciao.

Prova lo sketch per il calcolo del fattoriale seguente:

long fattoriale(int n) {
  if (n<2) {  
    return 1;
  } else {
    Serial.println("chiamata ricorsiva");
    return n * fattoriale(n - 1);
   
  }
}

void setup() {
    Serial.begin(115200);
    long f = fattoriale(4);
    Serial.println(f);

}

void loop () {
    // vuoto
}

Ciao.

Nuova versione del fattoriale:

long fattoriale(int n) {
  if (n<2) {  
    return 1;
  } else {
    Serial.print("f = ");
    Serial.print(n);
    Serial.print(" * fattoriale(");
    Serial.print(n - 1);
    Serial.println(");");

    long f = n * fattoriale(n - 1);
    Serial.println(f);
    return f;
   
  }
}

Meno male che non hai inserito anche i suoceri, altrimenti si andava a finire male :smiley:

1 Like

si può provare senza caricarlo su arduino?

Si c'è il simulatore wokwi.
Se non vuoi iscriverti apri un progetto esistente cancella il contenuto delle scketch e ci incolli quello del fattoriale poi run. Così però non puoi salvare il tuo progetto.
Per potere salvare i tuoi progetti devi iscriverti/loggarti e puoi usare l'account google se lo hai.

Ma fammi capire tu nella mia pagina del blog non ci sei mai entrato?

Ciao.