Menù con LCD 4x20 ed encoder

Salve, sicuramente l'argomento menù è stato trattato molte volte, ho visto anche che c'è una libreria apposita, però ho voluto lo stesso cimentarmi in questa programmazione. In un'altra discussione ho postato la realizzazione di un mio progetto, però ogni volta che vedevo il codice notavo che quello del menù era ridontante e ripetitivo, per cui ho deciso di creare un nuovo codice il più possibile snello. Rispetto al vecchio è nettamente superiore, ma rivedendolo penso che si possa fare ancora meglio. Posto il file perchè il codice è lunghetto, chi non ha niente da fare può dare uno sguardo e dirmi in cosa migliorare.

Menu.ino (10,7 KB)

Salve, il tuo file non funziona, ci sara' stato qualche problema nell'Upload. Tuttavia oramai scrivere un menu' per LCD 4x20 (immagino si stia parlando del famoso HD44780 con adattatore I2C ) per quanto utile e robusto per fare esperimenti, oramai credo sia obsoleto. Credo che sarebbe molto piu utile fare un menu' per nuovi display, tipo gli Oled SSD1306, costano poco oramai e sopratutto consumano pochissimo paragonati al 4X20 in questione. Ovviamente la mia e' solo una considerazione personale, ma non ti nascondo che spesso anche io uso quei display, giusto per iniziare un progetto, che dopo verso la versione finale mi metto con santa pazienza e riscrivo la parte LCD per OLED. Saluti, Gaetano.

E' lungo, ma non tanto da non poter essere pubblicato qui!

//  librerie per display LCD ompatibili con R1 v4
#include <Wire.h>
#include <PCF8574_HD44780_I2C.h>

// Definizione dei pin per l'encoder
#define  Clk 3
#define Dt 2
#define pin_pul 4

// Definizione variabili per i menù
int posizione = 0;   // Indica la posizione della freccia nel display
int scelta = 0;      // Indica quale opzione del menù si sceglie premendo il pulsante
bool tasto =0;     // 0 = pulsante non premuto, 1 = pulsante premuto
int direzione = 0;   // Indica il verso di rotazione dell'encoder, -1 o +1
int prevClk;
int prevDt;
int visualizza_menu =0;
bool esci=0;
// Definizione caratteristiche display
PCF8574_HD44780_I2C lcd(0x27,20,4);

// Definizione dei sottomenu
String menu_principale[5] = {"Scelta 1", "Scelta 2", "Scelta 3", "Scelta 4", "Esci"};
String sottomenu1[5] = {"Scelta 1_1", "Scelta 2_1", "Scelta 3_1", "Scelta 4_1", "Esci"};
String sottomenu2[9] = {"Scelta 1_2", "Scelta 2_2", "Scelta 3_2", "Scelta 4_2", "Scelta 5_2", "Scelta 6_2", "Scelta 7_2", "Scelta 8_2", "Esci"};
String sottomenu3[12] = {"Scelta 1_3", "Scelta 2_3", "Scelta 3_3", "Scelta 4_3", "Scelta 5_3", "Scelta 6_3", "Scelta 7_3", "Scelta 8_3", "Scelta 9_3", "Scelta 10_3", "Scelta 11_3", "Esci"};
String sottomenu4[5] = {"Scelta 1_4", "Esci"};

void setup() {
  // Inizializzazione dell'encoder
  pinMode(Clk, INPUT);
  pinMode(Dt, INPUT);
  pinMode(pin_pul, INPUT_PULLUP);

  // Inizializzazione del display
  lcd.init();                      
  lcd.backlight();
  Serial.begin(9600);
  prevClk = digitalRead(Clk);
  prevDt = digitalRead(Dt);

}

void loop() {
  // Visualizzazione del menù principale se si preme il pulsante dell'encoder
  if (digitalRead(pin_pul) == 0) Menu_principale();
  //Serial.println("Loop principale"); 
}

void Menu_principale() {
  while(digitalRead(pin_pul) !=1 ){} // Gira finchè non viene rilasciato il pulsante
  lcd.clear();
  scelta =0;
  posizione = 0;
  tasto = 1;
  esci =0;
  visualizza_menu =1;
  // Stampa il menù principale
  stampa_menu(0,4,menu_principale);
  //stampa_freccia();  
  while (esci==0) {  // Esce dal menù quando viene scelta l'opzione desiderata
   stampa_freccia();
   leggi_Encoder();
   if (posizione > 3) {
      visualizza_menu=2;
      posizione = 0;
   }  else if (posizione ==1 && visualizza_menu ==2){
       visualizza_menu =1;
       posizione = 0;
       scelta = 0;      
     }
    if (posizione < 0 ){
       if (visualizza_menu ==1) {
         visualizza_menu =2;
         posizione = 0;
         scelta = 4;
       } else if (visualizza_menu ==2){
         visualizza_menu =1;
         posizione = 3;
         scelta = 3;      
       }
     }
    switch(visualizza_menu){
     case 1:
       stampa_menu(0,4,menu_principale);
     break;
     case 2:
       stampa_menu(4,5,menu_principale);
     break;
    }
    if (tasto==0) {
     switch(scelta){
       case 0:
       prima_scelta();
       break;
       case 1:
       seconda_scelta();
       break;
       case 2:
       terza_scelta();
       break;
       case 3:
       quarta_scelta();
       break;
       case 4:
       esci=1;
       break;
     } // Fine switch
   } // fine IF
  }  // Fine while
  while(digitalRead(pin_pul) !=1 ){} // Si ferma finchè non viene rilasciato il tasto
  lcd.clear();
}  //  Fine menù principale

void prima_scelta() {
  while(digitalRead(pin_pul) !=1 ){} // Gira finchè non viene rilasciato il pulsante
  lcd.clear();
  scelta=0;
  posizione = 0;
  tasto = 1;
  esci=0;
  // Stampa il menù principale
  stampa_menu(0,4,sottomenu1);
  //stampa_freccia();  
  while (esci==0) {  // Esce dal menù quando viene scelta l'opzione desiderata
   stampa_freccia();
   leggi_Encoder();
     if (posizione > 3) {
       visualizza_menu=2;
       posizione = 0;
     }  else if (posizione ==1 && visualizza_menu ==2){
       visualizza_menu =1;
       posizione = 0;
       scelta = 0;      
     }
     if (posizione < 0 ){
       if (visualizza_menu ==1) {
         visualizza_menu =2;
         posizione = 0;
         scelta = 4;
       } else if (visualizza_menu ==2){
         visualizza_menu =1;
         posizione = 3;
         scelta = 3;      
       }
     }
    switch(visualizza_menu){
     case 1:
       stampa_menu(0,4,sottomenu1);
     break;
     case 2:
       stampa_menu(4,5,sottomenu1);
     break;
    }
    if (tasto==0) {
     switch(scelta){
       case 0:
       prima_scelta();
       break;
       case 1:
       //seconda_scelta();
       break;
       case 2:
       //terza_scelta();
       break;
       case 3:
       //quarta_scelta();
       break;
       case 4:
       esci=1;
       break;
     } // Fine switch
   } // fine IF
  }  // Fine while
  while(digitalRead(pin_pul) !=1 ){} // Si ferma finchè non viene rilasciato il tasto
  lcd.clear();
  Menu_principale();
}  //  Fine prima_scelta

void seconda_scelta() {
  while(digitalRead(pin_pul) !=1 ){} // Gira finchè non viene rilasciato il pulsante
  lcd.clear();
  scelta=0;
  posizione = 0;
  tasto = 1;
  esci=0;
  visualizza_menu =1;
  // Stampa il menù principale
  stampa_menu(0,4,sottomenu2);
  //stampa_freccia();  
  while (esci==0) {  // Esce dal menù quando viene scelta l'opzione desiderata
   stampa_freccia();
   leggi_Encoder();
     if (posizione > 3) {
       if (visualizza_menu==1) {
         visualizza_menu=2;
         posizione = 0;
       }  else if (visualizza_menu==2){
         visualizza_menu =3;
         posizione = 0;      
       }  
     } 
     if (posizione ==1 && visualizza_menu==3){
        visualizza_menu =1;
        posizione = 0; 
        scelta =0;     
      }
     if (posizione < 0){
       if (visualizza_menu==1) {
         visualizza_menu =3;
         posizione = 0;
         scelta = 8;
       } else if (visualizza_menu ==2){
         visualizza_menu =1;
         posizione = 3;
         scelta = 3;      
       } else if (visualizza_menu ==3){
         visualizza_menu =2;
         posizione = 3;
         scelta = 7; 
       }
     }
    switch(visualizza_menu){
     case 1:
       stampa_menu(0,4,sottomenu2);
     break;
     case 2:
       stampa_menu(4,8,sottomenu2);
     break;     
     case 3:
       stampa_menu(8,9,sottomenu2);
      break;
    }
    if (tasto==0) {
     switch(scelta){
       case 0:
       prima_scelta();
       break;
       case 1:
       //seconda_scelta();
       break;
       case 2:
       //terza_scelta();
       break;
       case 3:
       //quarta_scelta();
       break;
       case 4:
       //quinta_scelta();
       break;
       case 5:
       //sesta_scelta();
       break;
       case 6:
       //settima_scelta();
       break;
       case 7:
       //ottava_scelta();
       break;
       case 8:
       esci=1;
       break;
     } // Fine switch
   } // fine IF
  }  // Fine while
  while(digitalRead(pin_pul) !=1 ){} // Si ferma finchè non viene rilasciato il tasto
  lcd.clear();
  Menu_principale();
}  //  Fine seconda_scelta

void terza_scelta() {
  while(digitalRead(pin_pul) !=1 ){} // Gira finchè non viene rilasciato il pulsante
  lcd.clear();
  scelta=0;
  posizione = 0;
  tasto = 1;
  esci=0;
  visualizza_menu =1;
  // Stampa il menù principale
  stampa_menu(0,4,sottomenu3);
  //stampa_freccia();  
  while (esci==0) {  // Esce dal menù quando viene scelta l'opzione desiderata
   stampa_freccia();
   leggi_Encoder();
   if (posizione > 3) {
      if (visualizza_menu==1) {
        visualizza_menu=2;
        posizione =0;
     }  else if (visualizza_menu==2){
         visualizza_menu =3;  
         posizione =0;  
       }  
   }   
   if (posizione ==3 && visualizza_menu==3){
      visualizza_menu =1;
      posizione = 0; 
      scelta =0;     
    }
    if (posizione < 0){
      posizione = 3;
      if (visualizza_menu==1) {
        visualizza_menu =3;
        scelta = 11;
      } else if (visualizza_menu ==2){
         visualizza_menu =1;
         scelta = 3;      
       } else if (visualizza_menu ==3){
         visualizza_menu =2;
         scelta = 7; 
       }
     }
    switch(visualizza_menu){
     case 1:
       stampa_menu(0,4,sottomenu3);
     break;
     case 2:
       stampa_menu(4,8,sottomenu3);
     break;     
     case 3:
       stampa_menu(8,12,sottomenu3);
     break;
    }
    if (tasto==0) {
     switch(scelta){
       case 0:
       prima_scelta();
       break;
       case 1:
       //seconda_scelta();
       break;
       case 2:
       //terza_scelta();
       break;
       case 3:
       //quarta_scelta();
       break;
       case 4:
       //quinta_scelta();
       break;
       case 5:
       //sesta_scelta();
       break;
       case 6:
       //settima_scelta();
       break;
       case 7:
       //ottava_scelta();
       break;
       case 11:
       esci=1;
       break;
     } // Fine switch
   } // fine IF
  }  // Fine while
  while(digitalRead(pin_pul) !=1 ){} // Si ferma finchè non viene rilasciato il tasto
  lcd.clear();
  Menu_principale();
}  //  Fine terza_scelta

void quarta_scelta() {
  while(digitalRead(pin_pul) !=1 ){} // Gira finchè non viene rilasciato il pulsante
  lcd.clear();
  scelta=0;
  posizione = 0;
  tasto = 1;
  esci=0;
  // Stampa il menù principale
  stampa_menu(0,2,sottomenu4);
  //stampa_freccia();  
  while (esci==0) {  // Esce dal menù quando viene scelta l'opzione desiderata
   stampa_freccia();
   leggi_Encoder();
   if (posizione > 1) {
      posizione = 0;
      scelta =0;
    } else if (posizione < 0 ){
      posizione = 1;
      scelta = 1;
    } 
    if (tasto==0) {
     switch(scelta){
       case 0:
       prima_scelta();
       break;
       case 1:
       esci=1;
       break;
     } // Fine switch
   } // fine IF
  }  // Fine while
  while(digitalRead(pin_pul) !=1 ){} // Si ferma finchè non viene rilasciato il tasto
  lcd.clear();
  Menu_principale();
}  //  Fine quarta_scelta

void stampa_menu(int min_menu,int max_menu, String sottomenu[]){  // Visualizza il menù su LCD. min_menu e max_menu dicono quale intervallo dell'array visualizzare
  lcd.clear();
  for (int i=min_menu; i<max_menu; i++) {
    lcd.setCursor(1, i-min_menu);
    lcd.print(sottomenu[i]);
  }
}

void stampa_freccia(){
  for (int i=0;i<4;i++){
   lcd.setCursor(0,i); 
   lcd.print(" ");
  }
  lcd.setCursor(0,posizione);
  lcd.print(">");  // Stampa la freccia sul LCD
}

void leggi_Encoder(){
   
  while(digitalRead(pin_pul) !=1 ){} // Gira finchè non viene rilasciato il pulsante
  direzione = 0;
  tasto = 1;
  while(direzione==0 && tasto==1){
   tasto = digitalRead(pin_pul); 
   int currClk = digitalRead(Clk);
   int currDt = digitalRead(Dt);
   if (currClk != prevClk) {
     if (0 == currClk) { 
       direzione = 1 - ((currDt == currClk) << 1);
     }
     prevClk = currClk;
     prevDt = currDt;
    }
  } // Fine while
  if (direzione > 0) {
    scelta++;
    posizione++;
  } else if (direzione < 0) {
     scelta--;
     posizione--;
  }
  while(digitalRead(pin_pul) !=1 ){} // Gira finchè non viene rilasciato il pulsante
}

Ci sono molte osservazioni da fare... Solo per cominciare:

  • Le costanti, per riconoscerle, si usa scriverle maiuscole: CK, DT, PIN_PUL
  • pin_pul: sarebbe meglio PIN_PULSANTE
  • Per renderlo ancora più chiaro puoi mettere due define:
    #define PULSANTE digitalRead(PIN_PULSANTE)
    #define PREMUTO ==LOW
    e poi scrivere:
    if (PULSANTE PREMUTO)
    .
    oppure, con solo la define per la lettura:
    #define PULSANTE !digitalRead(PIN_PULSANTE) // Il pulsante chiude a massa.
    if (PULSANTE) // Premuto
    if (!PULSANTE) // Non premuto
  • Tutte quelle String possono essere facilmente evitate.

Molti anni fa progettai il mio contatore Geiger. Era solo il mio quarto progetto con Arduino, quindi avrei potuto strutturarlo un po' meglio. Comunque, negli anni l'ho corretto e ottimizzato parecchio:
GEIGERINO_v1.14g.zip (31.6 KB)
(questo è veramente lungo e diviso in più file!)
Questa è la funzione menu, con due righe che scorrono su un classico display 1604:

void Menu()
{
// Per l'azzeramento dopo pressione prolungata: con fatto==0, scrittura iniziale di ■■■■■■■■■■
byte fatto=0;
unsigned long t_barra; 
byte pos_cur=12;

lcd.clear(); lcd.print("> "); lcd.print(voce[cv]);
while(!(PIND&0x20)) // Attende che venga lasciato il pulsante.
  {
  if(millis()-t1>2000) // Se il pulsante è premuto da due secondi, disegna una barra decrescente e poi azzera i conteggi.
    {
    if (fatto==0)
      {
      fatto=1;
      lcd.clear(); lcd.setCursor(2,0); lcd.print("Azzeramento   ");
      lcd.setCursor(3,1); for(byte n=1; n<11; n++) {lcd.write(255);}
      t_barra=millis();
      }
    // Poi cancella un rettangolino per volta:
    else if (pos_cur>2 && millis()-t_barra>100) {t_barra+=100; lcd.setCursor(pos_cur,1); lcd.write(254); pos_cur--;}
    else if (pos_cur==2) {Biiip(); Azzera(); lcd.clear(); lcd.print("    Azzerato    "); delay(1000); return;}
    }
  } // Fune azzeramento.
if (fatto) return; // Se il pulsante viene lasciato prima della cancellazione di tutti i rettangolini ■, esce.
t1=millis();
while(PIND&0x20) // Continua a leggere l'encoder finché non premo:
  {
  encoder();
  if(E!=0) {cv+=E; t1=millis(); delay(20);}
  if(cv>9) {noTone(7); cv=9;} // Le voci vanno da 1 a 9.
  if(cv<1) {noTone(7); cv=1;}
  lcd.setCursor(2,0); lcd.print (voce[cv]); lcd.setCursor(4,1); lcd.print (voce[cv+1]);
  if(millis()-t1>4999) return; // Dopo 5 secondi di inattività esce.
  }
delay(200); lcd.clear();
switch(cv) // Ho premuto: salta alla funzione selezionata.
  {
  case 1: Integrazione(); break;
  case 2: Azzeramento(); break;
  case 3: massimi(); break;
  case 4: suoni(); break;
  case 5: volume(); break;
  case 6: allarme(); break;
  case 7: retroillum(); break;
  case 8: precisione(); break;
  case 9: autonomia(); break;
  }
}

Feci una discussione in proposito sul New Radioactivity Forum:
https://www.radioactivityforum.it/forum/viewtopic.php?f=19&t=2052

Pubblicai anche un video su Youtube, in cui si vede anche il menu:

Successivamente, sotto ai LED ho aggiunto un grande strumento ad ago con scala logaritmica.

Ho già qualche display oled, ho provato qualcosa giusto per vederne il funzionamento, ma per ora preferisco perfezionare la programmazione con l' HD44780, è più semplice da gestire. Ho già creato e venduto un piccolo progetto con il display SSD1306, un piccolo display oled 96x16 e un ESP32, però l'ho utilizzato solo per visualizzare informazioni. Sicuramente, quando avrò acquisito più esperienza nella programmazione, passerò a questo tipo di display

Mi piace questo sistema, è più leggibile, lo adotterò.
Perchè sarebbe meglio PIN_PULSANTE?
Mi sono piaciute anche le tue osservazioni, non sapevo si potesse programmare in quel modo. Le attuo subito.

Una cosa che non mi piace ancora, è il solito encoder, a volte, anche girando sempre nello stesso verso, torna indietro e poi va avanti, oppure anzichè scendere di una riga alla volta, scende di due righe alla volta, però essendo un menù, questo tipo di anomalia non crea danni.

Ho provato il tuo codice al simulatore dove non si nota il problema dell'encoder che hai specificato. Tuttavia in un menu (non ricordo quale) per selezionare l'opzione Esci c'è qualche problema.
Considera che i contatti possono rimbalzare e che il codice che impegna la CPU non permette di eseguire la routine di lettura encoder. La situazione potrebbe migliorare con i condensatori sui pin CLK e DT o comunque un rete RC antirimbalzo.

Con questa implementazione durante la navigazione del menu l'applicazione è in pausa. Anche il codice di @Datman si comporta così. Per verificarlo basta mettere nel loop un blink che smette di lampeggiare quando si entra nel menu.

Il difficile è cedere il controllo alla funzione loop nel più breve tempo possibile. Ragionandoci su, quando un evento si verifica stampiamo sul display tutte le opzioni che entrano in una pagina (4 righe) e cediamo il controllo alla funzione loop al prossimo ciclo stampiamo il carattere selettore ">" per evidenziare l'opzione corrente e cediamo il controllo al loop. Un altro evento ad esempio encoder incrementato e cancelliamo > della opzione corrente e lo stampiamo accanto alla opzione seguente che diventa quella corrente e cediamo il controllo al loop. Per fare ciò però ci possiamo scordare di usare while ed in genere tutti i cicli innestati dentro al ciclo loop.

Così facendo però ci manca l'associazione tra elemento del menu corrente e la funzione da chiamare quando confermiamo la scelta.
Possiamo selezionare la funzione come ha fatto @Datman attraverso lo switch, In ogni caso la funzione chiamata non deve detenere il controllo per lungo tempo, ma al più presto cedere i controllo alla funzione loop.

Questo è più o meno l'obbiettivo da raggiungere che a quanto pare è poco intuitivo da realizzare e infatti è pieno il forum di codice che si comporta detenendo il controllo per un tempo anche infinito senza mai cederlo alla funzione loop.

Ciao.

Eravamo partiti dal presupposto che durante l'accesso al menu si potesse bloccare il funzionamento di tutto il resto.

Il problema dei falsi conteggi si verifica quando si rileva il movimento semplicemente leggendo il livello di un segnale in corrispondenza del fronte dell'altro. Apposta io faccio diversamente!

Curiosità personale: con il tuo sistema cosa succede se si comincia a leggere l'encoder mentre sta già venendo mosso (e quindi può trovarsi in un punto qualsiasi del codice gray diverso da quello di riposo)?

Funziona regolarmente... :slight_smile:
Se guardi nel codice, rileva un movimento solo dopo che è passato per lo zero. Proprio per questo è immune dai rimbalzi!

Sarebbe interessante vedere se verrebbe trovato poco intuitivo anche da chi non fosse inizialmente esposto al codice procedurale...

Non mi ero accorto di altro comportamento che non desidero, cioè le funzioni:
stampa_freccia() e stampa_menu() sono eseguite in un ciclo infinito condizionato dalla variabile esci.

Ho provato ad apportare piccole modifiche per evitare di ripetere ciò che è stato già visualizzato ma non sono riuscito nell'intento senza stravolgere tutto lo sketch.

Ciao.

Datman perchè sarebbe meglio PIN_PULSANTE al posto di pin_pul?

Perchè non va bene? A me sembra logico e semplice nell'esecuzione, quando viene scelta l'opzione desiderata esce dal ciclo.

Quando nel loop principale viene premuto il pulsante, si trasferisce tutto il controllo a l menù, il loop principale non viene più eseguito perchè questi menù cambieranno variabili utilizzate nel loop. Quando dal menù principale si esce, si torna al loop principale e tutte le istruzioni useranno le variabili cambiate nei menù.

Dove posso trovare uno schema di rete anti rimbalzo?

Esatto, è nella terza scelta del menù principale, quando prremo su esci, anzichè andare nel menù principale, va alla prima scelta. Il bello è che in quella funzione non c'è un riferimento alla prima scelta, non so come ci vada