Projektvorstellung: Menü für LCD Display mit Rotary Encoder Auswahl

Hallo,
nachdem ich vor kurzem ein 4x16 Display zusammen mit einer I2C Schnittstelle in Betrieb genommen habe, musste eine einfache Menüsteuerung her.
Die vorhandenen LCD Menüs waren mir alle zu komplex und vor allem wollte ich die Steuerung mit einem Rotary Encoder vornehmen.
Das Ergebnis wollte ich euch nicht vorenthalten, vielleicht gibt es ja weitere Interessenten.
Doch zunächst zu meinem Aufbau, den ich zunächst zur Entwicklung genutzt habe (mit Angabe der Bezugsquelle):
Arduino IDE022
Arduino Mega2560
4x16 LCD Display LCD-Modul TC1604A-01 online kaufen | Pollin.de
I2C LCD Display Controller Bausatz LCD/I²C-Modul online kaufen | Pollin.de

Die Menueeinträge werden direkt im Code vorgenommen. Wenn das Display weniger Zeilen als Menueeinträge hat, werden die Menueeinträge durchgescrollt.
Ich habe den Code so gebaut, dass er auch mit einem Standard LCD Display funktioniert, dafür den #define Eintrag auskommentieren. Getestet wurde das mit einem 2x16 Standarddisplay. Bei einem Standarddisplay gehen nicht alle Funktionen verglichen zum I2C Display. Die im Code verwedeten unterschiedlichen Funktionen wurden deshalb auch in #ifdef Statements eingebunden.

Mit dem Encoder scrollt man durch die Menueeinträge (auf und ab). Wenn der letzte Menüeintrag erreicht wird, springt die Anzeige wieder nach vorne und umgekehrt. Ein Druck auf den Encoder wählt den Menüeintrag aus und nachfolgend können davon abhängige Programmteile angesteuert werden.
Der Code ist ausführlich kommentiert und sollte so auch von Anfängern verstanden werden. Zum debuggen sind noch Funktionen für die serielle Schnittstelle im Code (momentan auskommentiert).
Sicher kann man vieles besser, einfacher oder eleganter machen. Ich zähle auf jeden Fall noch lange nicht zu den Profis.
An dieser Stelle vielen Dank an Nick Gammon, von ihm stammt der Code für den Encoder.
Hier mein Code:

// Simple menu for LCD displays. 
// Author: Reinhard Nickels 02.01.2012
// Selection of menu item with a rotary encoder (eg. http://www.pollin.de/shop/dt/Njg2OTU3OTk-/Bauelemente_Bauteile/Passive_Bauelemente/Potis_Trimmer_Encoder/Encoder_PANASONIC_EVEQDBRL416B.html)
// Rotary encoder example based on http://arduino.cc/forum/index.php/topic,62026.msg449681.html#msg449681 from Nick Gammon

// Wiring: Connect common pin of encoder to ground.
// Connect pin A (one of the outer ones) to a pin that can generate interrupts (eg. D2)
// Connect pin B (the other outer one) to another free pin (eg. D5)

// Code was developed for I2C LCD displays, selection should be done with #define I2CDISPLAY value.
// Debug with serial interface can be activated with define DEBUG

//#define DEBUG    
int dummy;    // this a workaround to avoid error messages with #define and #ifdef, see http://code.google.com/p/arduino/issues/detail?id=206
#define I2CDISPLAY

#define MAXLINES 4    // defines the number of display lines
#define LCD_CHARACTERS 16

#ifdef I2CDISPLAY
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
#define LCD_ADRESS 0x27
LiquidCrystal_I2C lcd(LCD_ADRESS,LCD_CHARACTERS,MAXLINES);  // I2C LCD address is 0x27
#else
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);  // RS, E, D4, D5, D6, D7 - the standard connection
#endif


#define PINA 2
#define PINB 7
#define PUSHP 3
#define INTERRUPT 0  // that is, pin 2
#define INTERRUPTB 1  // that is, pin 3

volatile boolean turned;   // rotary was turned
volatile boolean fired;    // knob was pushed
volatile boolean up;  // true when turned cw

int CursorLine = 0;
int DisplayFirstLine = 0;
char* MenueLine[] = {"0. Hello","1. Test","2. BlaBla","3. something","4. pre Bot Line","5. End"};
int MenueItems;

// Interrupt Service Routine for a change to pushpin
void isrp ()
{
  if (!digitalRead (PUSHP))
    fired = true;
}  // end of isr

// Interrupt Service Routine for a change to encoder pin A
void isr ()
{
  if (digitalRead (PINA))
    up = digitalRead (PINB);
  else
    up = !digitalRead (PINB);
  turned = true;
}  // end of isr


void setup ()
{
  digitalWrite (PINA, HIGH);     // enable pull-ups
  digitalWrite (PINB, HIGH); 
  digitalWrite (PUSHP, HIGH); 
  attachInterrupt (INTERRUPT, isr, CHANGE);   // interrupt 0 is pin 2
  attachInterrupt (INTERRUPTB, isrp, FALLING);   // interrupt 5 is pin 18
#ifdef I2CDISPLAY
  lcd.init();
  lcd.backlight();
#else
  lcd.begin(LCD_CHARACTERS, MAXLINES);
#endif
  lcd.setCursor(0,0);
  lcd.print("Testing Menue!");
  lcd.setCursor(0,1);
  lcd.print("SW Ver 1.0");
  delay(1500);
  MenueItems = sizeof(MenueLine)/sizeof(MenueLine[0]);
#ifdef DEBUG  
  Serial.begin (9600);
  Serial.println("Testing Menue!........");
  Serial.print("Number of Menue Items ");
  Serial.println(MenueItems);
#endif
  lcd.clear();
  print_menue();
#ifdef I2CDISPLAY
  lcd.cursor_on();
#else
  lcd.cursor();
#endif  
  lcd.blink();
}  // end of setup

void loop ()
{
  if (turned)
    {
    if (up)
      move_up();
    else
      move_down();
    turned = false;
    }
  else if (fired)
    {
    selection();
    fired = false;
    }  // end if fired
}  // end of loop

void print_menue() {
  lcd.clear();
  for (int i=0;i<MAXLINES;i++) {
    lcd.setCursor(0,i);
    lcd.print(MenueLine[DisplayFirstLine + i]);
  }
  lcd.setCursor(0,(CursorLine-DisplayFirstLine));
}
 
void move_down() {
  if (CursorLine == (DisplayFirstLine+MAXLINES-1)) {
    DisplayFirstLine++;
  }
  if (CursorLine == (MenueItems-1)) {
    CursorLine = 0;
    DisplayFirstLine = 0;
  }
  else {
    CursorLine=CursorLine+1;
  }
  print_menue();
} 

void move_up() {
  if ((DisplayFirstLine == 0) & (CursorLine == 0)) {
    DisplayFirstLine = MenueItems-MAXLINES;   
  } 
  else if (DisplayFirstLine == CursorLine) {
    DisplayFirstLine--;
  }
  if (CursorLine == 0) {
    CursorLine = MenueItems-1;
  }
  else {
    CursorLine=CursorLine-1;
  }
  print_menue();
}

void selection() {
// integrate here your calls to selected sub (eg. with switch/case)
#ifdef DEBUG  
  Serial.print("Menueline ");
  Serial.print(CursorLine);
  Serial.println(" selected");
  Serial.print("..this is Menuetext ");
  Serial.println(MenueLine[CursorLine]);
#endif
  lcd.clear();
  lcd.print("You selected....");
  lcd.setCursor(0,1);
  lcd.print("Menue ");
  lcd.print(CursorLine);
  delay(1000);
  print_menue();  
}

Wenn man den konditionalen Code wegnimmt, bleibt ein gut überschaubarer Anteil übrig. Da die oben beschriebene HW Ausstattung meine Entwicklungsumgebung darstellt, wird dieser Code sicher bei mir noch oft zum Einsatz kommen. Im übrigen will ich nicht mehr auf die I2C LCD Ansteuerung verzichten. 4 Drähte statt 8 und zusätzlich programmgesteuertes Backlight - da bleiben Pins frei und der Breadboard Aufbau übersichtlich.

Wie immer: jeder Kommentar ist willkommen.
Gruß Reinhard

Hallo erni-berni,

herzlichen Dank für Deine Projektvorstellung.
So etwas suche ich schon länger.
Ich werde Dein Projekt demnächst mal selbst testen.

Danke Kurti

Könntest du vielleicht noch ein Beispiel Programmablauf in dein Menü einpflegen, damit man ungefähr weiß wo und wie man den eigenen Programmcode einpflegen kann? Das wäre schön wenn du mir das mal zeigen könntest :cold_sweat:
Ich bräuchte sowas in der Art

Programm 1
--> Überschreiten/Unterschreiten (Wert über encoder einstellen und bestätigen)
--> Schwellwert einstellen
--> + - (Wert über encoder einstellen und bestätigen)
--> Programm starten

Programm 2
--> Schwellwert 1 einstellen
--> + - (Wert über encoder einstellen und bestätigen)
--> Schwellwert 2 einstellen
--> + - (Wert über encoder einstellen und bestätigen)
--> Programm starten

Hallo omek,
danke für dein Interesse an diesem Projekt.
Ich habe hier einen Code für die Anzeige und das Stellen von Datum und Uhr. Da ist eigentlivh alles drin, was du suchst.
Ich muss den Code auf 2 Foreneinträge aufteilen.

// Simple menu for LCD displays. 
// Author: Reinhard Nickels 02.01.2012
// Selection of menu item with a rotary encoder (eg. http://www.pollin.de/shop/dt/Njg2OTU3OTk-/Bauelemente_Bauteile/Passive_Bauelemente/Potis_Trimmer_Encoder/Encoder_PANASONIC_EVEQDBRL416B.html)
// Rotary encoder example based on http://arduino.cc/forum/index.php/topic,62026.msg449681.html#msg449681 from Nick Gammon

// Wiring: Connect common pin of encoder to ground.
// Connect pin A (one of the outer ones) to a pin that can generate interrupts (eg. D2)
// Connect pin B (the other outer one) to another free pin (eg. D5)

// Code was developed for I2C LCD displays, selection should be done with #define I2CDISPLAY value.
// Debug with serial interface can be activated with define DEBUG

//#define DEBUG    
int dummy;    // this a workaround to avoid error messages with #define and #ifdef, see http://code.google.com/p/arduino/issues/detail?id=206
#define I2CDISPLAY

#define MAXLINES 4    // defines the number of display lines
#define LCD_CHARACTERS 20

#ifdef I2CDISPLAY
#include <Wire.h> 
#include <LiquidCrystal_I2C.h>
#define LCD_ADRESS 0x27
LiquidCrystal_I2C lcd(LCD_ADRESS,LCD_CHARACTERS,MAXLINES);  // I2C LCD address is 0x27
#else
#include <LiquidCrystal.h>
LiquidCrystal lcd(12, 11, 5, 4, 3, 2);  // RS, E, D4, D5, D6, D7 - the standard connection
#endif

#include <PCF8583.h>
int correct_address = 0;
PCF8583 p (0xA0);

static const byte sensorPin = 0;

#define PINA 2
#define PINB 7
#define PUSHP 3
#define INTERRUPT 0  // that is, pin 2
#define INTERRUPTB 1  // that is, pin 3

volatile boolean turned;   // rotary was turned
volatile boolean fired;    // knob was pushed
volatile boolean up;  // true when turned cw

int CursorLine = 0;
int DisplayFirstLine = 0;
char* MenueLine[] = {"0. Uhr zeigen","1. Uhr setzen","2. Datum setzen","3. sync Sekunden","4. pre Bot Line","5. End"};
int MenueItems;

// Interrupt Service Routine for a change to pushpin
void isrp ()
{
  if (!digitalRead (PUSHP))
    fired = true;
}  // end of isr

// Interrupt Service Routine for a change to encoder pin A
void isr ()
{
  if (digitalRead (PINA))
    up = digitalRead (PINB);
  else
    up = !digitalRead (PINB);
  turned = true;
}  // end of isr


void setup ()
{
  digitalWrite (PINA, HIGH);     // enable pull-ups
  digitalWrite (PINB, HIGH); 
  digitalWrite (PUSHP, HIGH); 
  attachInterrupt (INTERRUPT, isr, CHANGE);   // interrupt 0 is pin 2
  attachInterrupt (INTERRUPTB, isrp, FALLING);   // interrupt 5 is pin 18
#ifdef I2CDISPLAY
  lcd.init();
  lcd.backlight();
#else
  lcd.begin(LCD_CHARACTERS, MAXLINES);
#endif
  lcd.setCursor(0,0);
  lcd.print("Clock Menue!");
  lcd.setCursor(0,1);
  lcd.print("SW Ver 1.0");
  delay(1500);
  MenueItems = sizeof(MenueLine)/sizeof(MenueLine[0]);
#ifdef DEBUG  
  Serial.begin (9600);
  Serial.println("Testing Menue!........");
  Serial.print("Number of Menue Items ");
  Serial.println(MenueItems);
#endif
  lcd.clear();
#ifdef I2CDISPLAY
  lcd.cursor_on();
#else
  lcd.cursor();
#endif  
  lcd.blink();
  show_clock();   // show clock, waiting for fired
  print_menue();
}  // end of setup

void loop ()
{
  if (turned)
    {
    if (up)
      move_up();
    else
      move_down();
    turned = false;
    }
  else if (fired)
    {
    fired = false;
    selection();
    }  // end if fired
}  // end of loop

void print_menue() {
  lcd.clear();
  for (int i=0;i<MAXLINES;i++) {
    lcd.setCursor(0,i);
    lcd.print(MenueLine[DisplayFirstLine + i]);
  }
  lcd.setCursor(0,(CursorLine-DisplayFirstLine));
}
 
void move_down() {
  if (CursorLine == (DisplayFirstLine+MAXLINES-1)) {
    DisplayFirstLine++;
  }
  if (CursorLine == (MenueItems-1)) {
    CursorLine = 0;
    DisplayFirstLine = 0;
  }
  else {
    CursorLine=CursorLine+1;
  }
  print_menue();
} 

void move_up() {
  if ((DisplayFirstLine == 0) & (CursorLine == 0)) {
    DisplayFirstLine = MenueItems-MAXLINES;   
  } 
  else if (DisplayFirstLine == CursorLine) {
    DisplayFirstLine--;
  }
  if (CursorLine == 0) {
    CursorLine = MenueItems-1;
  }
  else {
    CursorLine=CursorLine-1;
  }
  print_menue();
}

void selection() {
// integrate here your calls to selected sub (eg. with switch/case)
#ifdef DEBUG  
  Serial.print("Menueline ");
  Serial.print(CursorLine);
  Serial.println(" selected");
  Serial.print("..this is Menuetext ");
  Serial.println(MenueLine[CursorLine]);
#endif
/*  lcd.clear();
  lcd.print("You selected....");
  lcd.setCursor(0,1);
  lcd.print("Menue ");
  lcd.print(CursorLine);
  delay(1000); */
  switch (CursorLine) {
    case 0:
      show_clock();
      break;
    case 1:
      set_clock();
      break;
    case 2:
      set_date();  
      break;
    case 3:
      sync_sec();  
      break;
    default:
      break;
  }
  print_menue();  
}

Gruß
Reinhard

und der 2. Teil

void set_clock() {
  int high_v;
  p.get_time();
  char buf[9];
  sprintf(buf,  "%02d:%02d:%02d",p.hour, p.minute, p.second);
  int laenge = sizeof(buf) - 1;
  lcd.clear();
  lcd.print(" Uhrzeit setzen ");
  lcd.setCursor(4,2);
  lcd.print(buf);
  for (int i=0;i<laenge;i++) {
    if (buf[i]==58)   // überspringe ":";
      i++;
    lcd.setCursor(i+4,2);
    switch (i) {
      case 0:
        high_v = 50;
        break;
      case 1:
        if (buf[i-1]==50)
          high_v = 51;
        else
          high_v = 57;
        break;
      case 3:
        high_v = 53;
        break;
      case 4:
        high_v = 57;
        break;
      case 6:
        high_v = 53;
        break;
      case 7:
        high_v = 57;
        break;
    }
    while (!fired) {
      if (turned) {
        if (up) {
          if (buf[i]== high_v)
            buf[i]=48;
          else  
            buf[i]++;
          lcd.print(buf[i]);
          lcd.setCursor(i+4,2);
        }
        else {
          if (buf[i]== 48)
            buf[i]=high_v;
          else  
            buf[i]--;
          lcd.print(buf[i]);
          lcd.setCursor(i+4,2);
        }
        turned = false;
      }
//      delay(200);
    }
    fired = false;
  }
/*      lcd.clear();
      lcd.print("Your selection:");
      lcd.setCursor(0,1);
      lcd.print(buf);
      delay(1000);
*/      
      p.hour = 10*(buf[0]-48) + buf[1]-48;
      p.minute = 10*(buf[3]-48) + buf[4]-48;
      p.second = 10*(buf[6]-48) + buf[7]-48;
      p.set_time();
}

void set_date() {
  int high_v;
  p.get_time();
  char buf[11];
  sprintf(buf, "%02d.%02d.%02d",p.day, p.month, p.year);
  int laenge = sizeof(buf) - 1;
  lcd.clear();
  lcd.print("  Datum setzen");
  lcd.setCursor(3,2);
  lcd.print(buf);
  for (int i=0;i<laenge;i++) {
    if (buf[i]==46)   // überspringe ".";
      i++;
    lcd.setCursor(i+3,2);
    switch (i) {
      case 0:
        high_v = 51;
        break;
      case 1:
        if (buf[i-1]==51)
          high_v = 49;
        else
          high_v = 57;
        break;
      case 3:
        high_v = 49;
        break;
      case 4:
        if (buf[i-1]==49)
          high_v = 50;
        else
          high_v = 57;
        break;
      case 6:
        high_v = 50;
        break;
      case 7:
        high_v = 49;
        break;
      case 8:
        high_v = 57;
        break;
      case 9:
        high_v = 57;
        break;
    }
    while (!fired) {
      if (turned) {
        if (up) {
          if (buf[i]== high_v)
            buf[i]=48;
          else  
            buf[i]++;
          lcd.print(buf[i]);
          lcd.setCursor(i+3,2);
        }
        else {
          if (buf[i]== 48)
            buf[i]=high_v;
          else  
            buf[i]--;
          lcd.print(buf[i]);
          lcd.setCursor(i+3,2);
        }
        turned = false;
      }
    }
    fired = false;
  }
/*      lcd.clear();
      lcd.print("Your selection:");
      lcd.setCursor(0,1);
      lcd.print(buf);
      delay(1000);
*/      
      p.day = 10*(buf[0]-48) + buf[1]-48;
      p.month = 10*(buf[3]-48) + buf[4]-48;
      p.year = 1000*(buf[6]-48) + 100*(buf[7]-48) + 10*(buf[8]-48) + buf[9]-48;
      p.set_time();
}

void show_clock() {
  lcd.clear();
  lcd.blink_off();
  lcd.cursor_off();
  while (!fired) {
    unsigned long starttime = millis();
    float temp = (float(analogRead((sensorPin)))/1024*5 -1.127)/0.00759;   // Read the temperature (in celsius)...
    lcd.setCursor(4, 1);
    p.get_time();
    char time[8];
    sprintf(time, "%02d:%02d:%02d",p.hour, p.minute, p.second);
    lcd.print(time);
    char date[10];
    sprintf(date, "%02d.%02d.%02d",p.day, p.month, p.year);
    lcd.setCursor(0, 3);
    lcd.print(date);
    lcd.setCursor(10, 3);
    lcd.print("      ");   // clear old data
    lcd.setCursor(10, 3);
    lcd.print(" ");
    lcd.print(temp, 1);
    lcd.print((char)223);   // Print the temperature (using the custom "degrees" symbol)...
    while((millis()-starttime < 500) & !fired){}
  }
  fired = false;
  turned = false;  // ignore if any
  lcd.blink_on();
}

void sync_sec() {
  show_clock();  
  if (p.second > 30)  {
    p.minute++;
  }
  p.second = 0;
  p.set_time();
  show_clock();  
  CursorLine=0;  // start with initial menue
}

Vielen Dank für deine Mühe! Da werd ich jetzt mal anfangen meinen Code da reinzumoscheln :grin:

:roll_eyes: Hab ich eine falsche Library??? :fearful:

sketch_aug17a:23: error: invalid conversion from 'int' to 't_backlighPol'
sketch_aug17a:23: error: initializing argument 3 of 'LiquidCrystal_I2C::LiquidCrystal_I2C(uint8_t, uint8_t, t_backlighPol)'
sketch_aug17a:31: error: 'PCF8583' does not name a type
/Applications/Arduino 2.app/Contents/Resources/Java/libraries/LiquidCrystal2/LiquidCrystal_I2C.h: In function 'void setup()':
/Applications/Arduino 2.app/Contents/Resources/Java/libraries/LiquidCrystal2/LiquidCrystal_I2C.h:154: error: 'int LiquidCrystal_I2C::init()' is private
sketch_aug17a:76: error: within this context
sketch_aug17a:95: error: 'class LiquidCrystal_I2C' has no member named 'cursor_on'
sketch_aug17a.cpp: In function 'void set_clock()':
sketch_aug17a:195: error: 'p' was not declared in this scope
sketch_aug17a.cpp: In function 'void set_date()':
sketch_aug17a:268: error: 'p' was not declared in this scope
sketch_aug17a.cpp: In function 'void show_clock()':
sketch_aug17a:349: error: 'class LiquidCrystal_I2C' has no member named 'blink_off'
sketch_aug17a:350: error: 'class LiquidCrystal_I2C' has no member named 'cursor_off'
sketch_aug17a:355: error: 'p' was not declared in this scope
sketch_aug17a:373: error: 'class LiquidCrystal_I2C' has no member named 'blink_on'
sketch_aug17a.cpp: In function 'void sync_sec()':
sketch_aug17a:378: error: 'p' was not declared in this scope
sketch_aug17a:381: error: 'p' was not declared in this scope

Hallo omek,
als erstes musst du die PCF8583 Library in deinen Library-Ordner von Arduino 0.22 einfügen.

Nur mit dieser Arduino-Version läuft der Sketch!

Ich habe noch hier zwei Änderungen gemacht:

void show_clock() {
  lcd.clear();
  lcd.noBlink();
  lcd.noCursor();

Danach lässt sich der Sketch ohne Fehler kompilieren

Gruß

Jürgen

Hi,
ist es schwierig, den Sketch mit der DS1307 Library abzuändern und es dann auch unter 1.0 laufen zu lassen?

Gruß

Hallo zusammen,

kann mir jemand sagen, wie ich es vermeide, nach Auswahl eines Untermenüs wieder zurück ins Hauptmenü zu springen, wenn man am Rotary Encoder dreht?

Ich komme an der Stelle nicht weiter. Ich will in dem Untermenü bleiben und dort Werte mit dem Rotary Encoder einstellen.

Danke euch!

Franzl

Franzl:
Hallo zusammen,

kann mir jemand sagen, wie ich es vermeide, nach Auswahl eines Untermenüs wieder zurück ins Hauptmenü zu springen, wenn man am Rotary Encoder dreht?

Ich komme an der Stelle nicht weiter. Ich will in dem Untermenü bleiben und dort Werte mit dem Rotary Encoder einstellen.

Danke euch!

Franzl

Wieso kaperst du einen uralten Thread ?

Mach für deine Frage einen neuen Thread auf und präzisiere deine Frage, so dass wir wissen, was du meinst.

Für alle Neuankömmlinge:

Ich weiss es ist ein alter Thread
Dennoch möchte ich dir Reinhard danke sagen für dieses einfach zu handhabende Menü. Bin erst jetzt darüber gestolpert. Habe aber erfolgreich ein Projekt damit umgesetzt! Dafür gibt's ein Karma von mir!

PS: Wichtig für andere nehmt das Uhren-Beispiel als Grundlage...beim ersten Thread gibt's noch Bugs die im Uhren-Beispiel ausgemerzt wurden :wink:

Gruss Lukacs